
Auditing GitHub Actions for CI Secret Theft: OIDC Migration Lessons from 4,837 Compromised Accounts
Two write-ups published on 13 November 2026 put a number on it: around 4,800 GitHub accounts, later revised to 4,837, were used to reach CI secrets and cloud credentials. Every summary I have read leads with that count. To me it is the least interesting part of the story. What follows is the audit pass I would run after reading those headlines: map what a repository's workflows can actually reach, read a workflow file the way a stolen account would, and migrate to OIDC without handing the benefit back through an over-broad trust policy.
If the reporting holds up, this was not a campaign against a bug in GitHub. It was a campaign against the trust graph every repository quietly builds: workflow files that can mint credentials, secrets readable by anything the workflow executes, and OIDC trust policies that decide which refs get a cloud role. Take over one account with branch write access and you do not need a vulnerability. Edit a YAML file, let your own CI do the theft. Account counts tell you scope; the shape of the trust graph tells you whether you are next.
On this page
What the public reporting establishes, and what it does not
The citable facts are thin, so I will keep them narrow.
TechRadar published "Thousands of GitHub repositories targeted in widespread attack" on 2026-11-13 at 15:20 UTC, reporting that roughly 4,800 accounts had been compromised to steal cloud credentials and CI secrets. ShiftDelete.Net published "GitHub Hack Uses 4,837 Stolen Accounts to Loot CI Secrets" the same day at 18:19 UTC, with the higher figure.
Both are secondary summaries. At the time of writing I have not found a primary GitHub advisory, a CVE record, a named threat actor, or an official incident timeline. That makes the following genuinely unknown rather than unreported-by-me:
- Initial access vector. Phishing, credential stuffing, session-token theft, a malicious OAuth app, a compromised third-party integration — all consistent with the headline numbers. I have evidence for none of them.
- What exactly was abused. Stolen browser sessions, classic personal access tokens, deploy keys, and GitHub App installation tokens leave very different forensic trails. The reports do not distinguish between them.
- Whether OIDC-trusted roles or static cloud keys were the target. This decides blast radius, and the summaries do not say.
- Whether the repositories were public, private, or both.
A count of "compromised accounts" with no primary source is a scope estimate, not a root cause. I will treat it that way for the rest of this post: the incident is the hook, but everything I verify below comes from GitHub's own documentation and CLI behaviour, not from the affected organisations.
A GitHub account is a build-pipeline credential, not a profile
Most people model a stolen GitHub account as "someone can push code or read private repos." That undersells the problem in exactly one direction, and it is the direction that matters.
Write access to src/app.ts is a code-integrity problem. Write access to .github/workflows/deploy.yml is a credential-minting problem. Both sit under the same permission bit — contents: write — and they are not the same risk. The workflow file is executable configuration that runs inside your runner with whatever secrets and identity tokens that runner is allowed to reach.
A stolen account does not need persistence here. No malware, no backdoor, no tampered build artifact. One commit adding a step, one workflow run, then throw the account away.
Enumerate what a repository can actually reach
Before auditing anything, map what a repo can pull into a runner. The classes below differ by scope, lifetime, and — critically — whether the credential is still replayable after you revoke repo access.
| Credential class | Scope | Typical blast radius | Replayable outside GitHub after repo access is revoked? |
|---|---|---|---|
GITHUB_TOKEN | Single job, single repo | Push/PR/package actions allowed by permissions: | No — expires with the job |
| Repository Actions secrets | One repo, all workflows | Whatever the secret unlocks | Depends entirely on the secret |
| Organization secrets | Selected or all repos | Cross-repo | Depends on the secret |
| Environment secrets | Jobs targeting that environment | Whatever the environment gates | Depends; protection rules may add approval |
| Classic PAT | All repos the user can see | User-equivalent API access | Yes, until expiry or revocation |
| Fine-grained PAT | Explicit repo + permission list | Narrow | Yes, but narrowly |
| Deploy key | One repo, SSH write or read | Git operations only | Yes, until removed |
| GitHub App installation token | Installation scope | App-defined permissions | No — one hour, but re-mintable while installed |
| OIDC-trusted cloud role | Condition-scoped | Cloud role permissions | No — short-lived, but re-mintable by any ref matching the trust policy |
The last two rows are the ones worth staring at. Both expire fast. Both are still fully mintable by anyone who can run a workflow that satisfies the trust condition. A short lifetime buys nothing when the thing doing the minting is editable by the attacker.
Why long-lived cloud keys are the worst case in this incident class
If a repository holds a static AWS access key, an Azure service-principal client secret, or a GCP service-account JSON, revoking the GitHub account does not end the compromise. That key is a bearer credential. It works from any IP, any ASN, any CLI. You removed the delivery mechanism; the credential is still valid and still out there.
I have watched this play out in tabletop exercises: the responder rotates the CI secret store, removes the compromised collaborator, closes the PR — and two weeks later the attacker is still calling sts:GetCallerIdentity from a VPS, because the key was exfiltrated to a note-taking app in the first hour.
An OIDC-minted token behaves differently. Minutes of lifetime, dead when the run ends. There is no ~/.aws/credentials file on an attacker laptop that keeps working. The only thing left to fix is the trust policy — and unlike a leaked key, a bad trust policy is configuration you can diff and revert.
That is the argument for OIDC. It is also incomplete, and I will get to why.
Reading a workflow file the way an attacker with a stolen session reads it
This is a deliberately generic deploy workflow. Nothing exotic, which is the point — it looks roughly like what most teams have.
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-west-1
- name: deploy
run: ./scripts/deploy.sh
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
An attacker with write access to a branch reads this as a list of levers, not as a build config.
| Line the attacker would touch | What they gain |
|---|---|
on: push → on: workflow_dispatch | A manual trigger they can fire repeatedly without merging anything |
on: push branch filter → their own branch | The workflow now runs from their ref, not main |
permissions: id-token: write | If absent, adding it makes an OIDC token mintable at all |
environment: production | Removing it may bypass required reviewers and environment-scoped secret gating |
role-to-assume → a different role ARN | Only useful if the trust policy is wildcarded enough to allow it — often it is |
Any added run: step | Arbitrary code with every env var in scope, plus the runner's network access |
aws-actions/configure-aws-credentials@v4 → an unpinned or lookalike ref | Persistent, quiet exfiltration on every future run |
The part people miss: the attacker never touches deploy.sh. They add four lines above it. Nobody reviews the diff carefully, because the diff lives in CI plumbing and CI plumbing is what everyone scrolls past.
Why trigger selection decides secret exposure and workflow trust
The on: block decides two things that get constantly conflated: whether secrets are in scope, and whether the workflow definition itself comes from trusted code.
| Trigger | Secrets available? | Workflow definition source | Notes |
|---|---|---|---|
push | Yes | Base repo, at the pushed commit | Attacker with branch write runs their own version of the workflow |
pull_request from a fork | No (read-only token, no secrets) | Base repo workflow, PR head code | The safe default for untrusted contributions |
pull_request_target | Yes | Base repo workflow, with github.event.pull_request.* from the fork | Dangerous only if you check out and execute head code |
workflow_run | Yes | Base repo | Runs privileged in the base repo context — same foot-gun as above if it consumes PR artifacts |
issue_comment | Yes | Default branch | Any commenter can trigger it; the definition is trusted but the inputs are not |
workflow_dispatch | Yes | Selected ref — including branches | Underrated: whoever can push a branch can often pick it in the UI |
The row I would underline is workflow_dispatch. The docs are accurate that it runs from a chosen ref, and most teams treat it as "manual button on main." In a repo with permissive branch push rights, it is a manual button on any branch the attacker created.
pull_request_target plus a head-repo checkout is the classic foot-gun
The dangerous pattern, in the shape it usually appears:
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: true
- run: npm ci && npm run build
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
pull_request_target runs in the base repo's context with secrets available — that is its whole purpose. The moment you check out head.sha, npm ci executes the fork author's postinstall script with NPM_TOKEN in the environment and a write-capable git credential sitting in .git/config. Not subtle, and still everywhere.
The fix is not "add a condition." Split the workflow:
## .github/workflows/pr-build.yml — untrusted code, no secrets
on: pull_request
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
The privileged job — the one that comments, labels, approves — goes in a separate pull_request_target workflow that never checks out the head and never runs head-controlled scripts. If a downstream job genuinely needs build output, upload an artifact and treat its contents as hostile input. Do not run them.
Same reasoning applies to workflow_run: it is privileged, so consuming an artifact from an unprivileged pull_request run and then executing it is the identical bug with one extra hop.
Where secrets leak without an attacker ever editing a workflow
Workflow edits are the loud path. These are the quiet ones:
- Third-party action supply chain.
uses: some-org/action@v1is a mutable tag. The maintainer — or whoever compromises the maintainer's account — can retagv1to different code and it runs with your secrets on the next build. Pin to a full commit SHA. Of all supply-chain controls, this is the one I would apply to every workflow today, with no exceptions for "trusted" publishers. - Debug logging.
set -xin a shell step, orACTIONS_STEP_DEBUG=true, will happily echo expanded environment variables into a log a lot of people can read. I have found live cloud keys this way during authorised reviews, printed by acurl -vinside a retry loop. - Artifacts and caches. Anything you upload includes whatever the step wrote to the workspace. A
cp .env dist/or a~/.aws/credentialsfile left in$HOMEduring a cache save ends up stored and downloadable. - Fork workflows that print
env. A "helpful" debug step in a reused template, copied into forty repos, dumping the whole environment on failure.
A reproducible audit pass with the GitHub CLI and API
Scope note: I tested the query mechanics and response shapes below against the documented API behaviour and a synthetic organisation. I have not run them against a live compromised org, and the numbers in the outputs are placeholders. The commands are real; treat the outputs as expected shapes.
Inventory every workflow and its effective permissions
Start with the repository list, then walk the contents API for each repo's workflow directory.
ORG=acme
gh repo list "$ORG" --limit 500 --json nameWithOwner \
--jq '.[].nameWithOwner' > repos.txt
wc -l repos.txt
142 repos.txt
Then enumerate workflow files:
while read -r repo; do
gh api "repos/$repo/contents/.github/workflows" --jq '.[] | select(.type=="file") | .path' 2>/dev/null | sed "s|^|$repo |"
done < repos.txt > workflows.txtacme/web .github/workflows/ci.yml
acme/web .github/workflows/deploy.yml
acme/infra .github/workflows/terraform-apply.yml
acme/mobile .github/workflows/release.yml
Now the setting people forget exists. The repository-level default for GITHUB_TOKEN covers every job that does not declare its own permissions: block:
gh api repos/acme/web/actions/permissions/workflow
{
"default_workflow_permissions": "write",
"can_approve_pull_request_reviews": true
}
That response is a finding. write means any job in any workflow without an explicit permissions: block gets a read-write token by default. can_approve_pull_request_reviews: true means a workflow token can approve pull requests — paired with branch protection requiring one approval, that is a self-approval path.
Find the dangerous triggers and missing default branch protection
Code search is convenient but lags indexing, so for an audit I shallow-clone and grep. Slower, and correct.
gh repo clone acme/web /tmp/audit/web -- --depth=1
grep -rnE 'pull_request_target|workflow_run' /tmp/audit/web/.github/workflows
/tmp/audit/web/.github/workflows/deploy.yml:2: pull_request_target:
/tmp/audit/web/.github/workflows/pr-check.yml:9: workflow_run:
And for unpinned action refs:
grep -rnE 'uses:\s+[^ ]+@(v[0-9]+|main|master)\s*$' /tmp/audit/web/.github/workflows
/tmp/audit/web/.github/workflows/ci.yml:18: - uses: actions/setup-node@v4
Triage table I would produce from that output:
| Repo | File | Finding | Severity | Why |
|---|---|---|---|---|
| acme/web | deploy.yml | pull_request_target | High — verify immediately | Privileged trigger; check for head checkout |
| acme/web | pr-check.yml | workflow_run consuming PR artifact | High | Same class as above if artifact is executed |
| acme/web | ci.yml | setup-node@v4 tag pin | Medium | Mutable ref with repo secrets in scope |
| acme/infra | terraform-apply.yml | No permissions: block | Medium | Inherits repo default of write |
Check environment protection rules and required reviewers
An environment secret is only as protected as the environment's rules. This query tells you whether the rule exists at all:
gh api repos/acme/web/environments \
--jq '.environments[] | {name: .name, rules: [.protection_rules[].type]}'
{"name":"production","rules":["required_reviewers","branch_policy"]}
{"name":"staging","rules":[]}
staging with an empty rules array is the finding here, not production. A production environment with required reviewers forces a human into the credential-minting path — a stolen account can trigger the workflow but cannot approve it. staging with no rules is just a namespace, and if a cloud credential is stored there it mints with no human involved at all. In my experience most teams review production carefully and forget the other four.
OIDC migration: what changes, and the three misconfigurations that give the benefit back
I will say it directly: OIDC is the right default, and I would migrate any repository that deploys to a cloud provider. But an over-broad OIDC trust policy is worse than a narrowly scoped static key, because at least the static key is a visible artifact you can rotate. A wildcard sub condition leaves nothing to look at — it does not look wrong in a diff, and it will happily mint credentials for the attacker's branch until someone audits the policy itself.
The OIDC token exchange flow you can read and verify
The runner exposes two variables when a job has id-token: write: ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN. You can request a token yourself and read it. Most people never look at this step.
permissions:
id-token: write
contents: read
jobs:
inspect:
runs-on: ubuntu-latest
steps:
- name: fetch and decode ID token
run: |
TOKEN=$(curl -sS \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
Decoded payload, abridged:
{
"aud": "sts.amazonaws.com",
"sub": "repo:acme/web:environment:production",
"ref": "refs/heads/main",
"repository": "acme/web",
"environment": "production",
"job_workflow_ref": "acme/web/.github/workflows/deploy.yml@refs/heads/main"
}
Read those claims carefully, because your cloud trust policy is written against them. sub decides which workflow runs get a role, and its format shifts with context: repo:org/repo:ref:refs/heads/main for a branch-scoped job, repo:org/repo:environment:production when the job declares environment: production, and repo:org/repo:pull_request for PR-triggered jobs.
Over-broad sub conditions in OIDC trust policies
This is the policy I find most often on AWS, and fixing it is the highest-value change in this entire walkthrough:
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:acme/*:*"
},
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
repo:acme/*:* means any repository in the organisation, on any ref — including a branch an attacker created thirty seconds ago from a single compromised account with push rights. The wildcard does not just widen trust. It converts "takeover of one low-privilege contributor" into "assume the production deploy role."
Tightened form:
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:acme/web:environment:production"
}
}
}
GCP's equivalent is an attribute condition on assertion.sub in the workload identity pool provider; Azure's is the subject field on the federated credential. Same claim, same mistake, three providers. I have seen repo:org/*:* on all three.
Audience pinning and the environment claim
aud exists so a token minted for one relying party cannot be replayed against another. Set it to what your provider expects — sts.amazonaws.com for AWS, the workload identity provider's expected audience for GCP, api://AzureADTokenExchange for Azure — and match it with StringEquals, never StringLike. A wildcard audience lets a token requested for your staging identity federation be presented to production.
The environment claim matters more operationally. If the trust policy requires sub to end in :environment:production, obtaining that token requires the job to declare environment: production — and an environment with required reviewers means a human approves the credential mint, not just the deploy. Of everything in GitHub Actions, that is the only mechanism I know that puts a person between "workflow runs" and "cloud role is assumed." It costs a click per deploy. For a production role, that click is cheap.
What OIDC does not fix
Here is the part the migration guides soft-pedal. OIDC eliminates static credentials at rest. It does not restore integrity of the workflow definition.
Give an attacker write access to a branch whose ref satisfies your tightened sub condition — refs/heads/main on a repo where they have merge rights, or any branch if you scoped by repository rather than environment — and they add an exfiltration step to your trusted workflow. The runner fetches a perfectly valid ID token, assumes your perfectly scoped role, and hands the resulting credentials to their run: step. Every control above was configured correctly.
What OIDC actually shrinks is the replay window: from "until someone notices the key" down to minutes. It does not stop the theft on the first run. That job belongs to branch protection on workflow files, required review, and least-privilege role permissions.
Shrinking the account blast radius before the next incident
Assume one account gets taken over. Everything below is about making that boring.
Replace classic PATs and GitHub App passwords with fine-grained PATs or App installation tokens
A classic PAT is user-equivalent access across every repo the user can see, with coarse scopes and long expiry. It is the single worst credential to leave floating around an organisation. Fine-grained PATs let you pin specific repositories and specific permissions with a mandatory expiry — I set 30 days and treat renewal as a prompt to check whether the token is still needed. GitHub App installation tokens are better still for automation: one-hour lifetime, a permission set defined by the app, and every token attributable to an installation rather than a human. The audit trail differs usefully too — App activity stays attributable even when the human who installed it is long gone.
Default GITHUB_TOKEN to read-only and declare per-job permissions
Change the organisation or repository default to read-only, then opt in explicitly:
permissions:
contents: read
jobs:
deploy:
permissions:
contents: read
id-token: write
The repo-level default matters even when your own workflows look tidy, because the next workflow someone adds — or copy-pastes from a blog post — inherits it. With the default at read-only, a forgotten permissions: block fails closed instead of silently granting write.
Rulesets, required reviewers on workflow files, and branch protection on the CI definition itself
.github/workflows/** deserves stricter protection than src/. If you have CODEOWNERS, add a rule requiring a platform or security reviewer for workflow paths, and make it required in the ruleset rather than advisory. Add a rule that blocks deletion of workflow files without review, because deleting a workflow that enforces a check is often easier than disabling the check.
The threat this closes is concrete: the attacker in this incident class does not need a novel technique. They need one unreviewed YAML diff on a branch that a wildcard trust policy accepts.
Detection: what a stolen account leaves in the audit log and where secret scanning falls short
Audit-log events worth alerting on
GitHub's audit log is the only place where the actions below remain attributable after the fact. Treat this table as a starting set of phrases to query — confirm the exact event names against the current GitHub audit-log documentation before building alerts on them, because names have changed and will change again.
| What you care about | Starting query against /orgs/{org}/audit-log | Why it matters |
|---|---|---|
| Workflow file creation or modification | phrase="action:workflows" | The primary move in this incident class |
| Environment protection changes | phrase="action:environment" | Removing required reviewers re-opens the mint path silently |
| PAT creation or scope change | phrase="action:personal_access_token" | Persistence that survives a password reset |
| Deploy key creation | phrase="action:public_key" | Quiet, long-lived git access |
| Actions secret create/update | phrase="action:secret" | Overwriting a secret can redirect a deploy |
| Org membership and role changes | phrase="action:org" | Privilege escalation before the credential theft |
| Branch protection overrides | phrase="action:protected_branch" | The pre-condition for pushing a workflow edit to main |
Two practical notes. First, alert on environment protection rule removal with the same urgency as on new PATs — it leaves no artifact except the audit entry. Second, action:workflows is noisy on active teams; scope the alert to changes on the default branch and to repos holding production cloud roles.
Secret scanning, push protection, and their blind spots
Secret scanning and push protection are genuinely effective at one thing: stopping a cloud key from being committed and pushed. That is a real win, and every organisation should have both enabled, including on private repos.
For this incident class they are close to useless. A secret passed to a step at runtime, never written to a file, exfiltrated over HTTPS to an attacker endpoint, leaves nothing for the scanner to find. Same for secrets base64-encoded into an uploaded artifact, or printed into a log the scanner does not parse.
What covers runtime secret theft is a canary secret. Create a cloud credential with no legitimate purpose — an IAM user named something like ci-canary-do-not-use, with a policy that grants nothing but logging. Store it as a repository secret. Then alert on any authentication attempt with it in CloudTrail, Azure Activity Log, or GCP audit logs. Legitimate workflows never use it, so anything that does is either a misconfiguration or an active compromise, and the alert fires from the cloud side where the attacker cannot see it. I would put one in every repo that holds cloud credentials, and I would put the detection query in the on-call rotation rather than on a dashboard nobody opens.
What I would fix first, in order
Ordered by risk removed per unit of effort, assuming a typical organisation with CI deploying to one cloud. The lesson I take from this incident class is that the biggest wins are configuration changes you can diff and review, not another scanner.
- Pin and scope OIDC trust conditions. Highest value, lowest effort, one line per role. Replace every
repo:org/*:*with a specific repository plus:environment:<name>where a human approval exists. On its own, this moves most of the attack path in this incident from "assume production role" to "assume nothing." - Kill long-lived cloud keys. Inventory every static key in every secret store and delete the ones you can replace with OIDC. This is the only step that makes an already-exfiltrated credential stop working. Tedious, and the one I would not defer.
- Review every
pull_request_targetandworkflow_runworkflow. There are usually fewer than you fear, and the risky ones stand out once you know to look for a head checkout. Split privileged and unprivileged workflows. - Default token permissions to read-only, org-wide. One setting, inherited by every future workflow, fails closed on forgetfulness.
- Audit-log alerting on environment protection changes and PAT creation. Detection rather than prevention — but it is what tells you an incident is happening while the credential is still short-lived.
What I would not spend time on yet: aggressive per-repository secret rotation schedules. Rotation sounds responsible and mostly generates churn. A credential that is short-lived and minted per run has nothing to rotate; one that is long-lived and reachable from CI does not stop being stolen because you rotate it quarterly — you just get a hygiene checkbox. Put that effort into step 1.
What I confirmed versus what I did not test
Confirmed against primary GitHub documentation and documented API behaviour: the permissions model and the id-token: write requirement; the environment protection rule types returned by the environments API; the shape of the default_workflow_permissions response; the OIDC token request via ACTIONS_ID_TOKEN_REQUEST_URL with the audience parameter; the claim set in the resulting JWT (sub, aud, ref, environment, job_workflow_ref); and the documented difference in secret availability between pull_request from a fork and pull_request_target.
Taken only from secondary reporting: the 4,800 and 4,837 account figures, the claim that CI secrets and cloud credentials were the objective, and the 13 November 2026 timeline. Both sources are news summaries. I have not seen a primary advisory, and I am not going to present their numbers as vendor-confirmed.
Inference, labelled as such: that this was an account-takeover-to-workflow-edit path rather than exploitation of a GitHub vulnerability. That reading follows from the reported objective and the mechanics above, but it is my interpretation of two secondary summaries, not a stated conclusion from either. The initial access vector, the credential type abused, and whether OIDC roles or static keys were targeted are all unknown to me — if a primary advisory later contradicts anything above, the advisory wins.
Further Reading
Primary documentation for the mechanics in this post:
- About security hardening with OpenID Connect — the OIDC claim set and trust condition format
- Configuring OpenID Connect in Amazon Web Services — the reference workflow used above
- Security hardening for GitHub Actions — permissions, pinned actions,
pull_request_targetguidance - Automatic token authentication —
GITHUB_TOKENscope and defaults - Using secrets in GitHub Actions — environment secrets and fork behaviour
- Managing environments for deployment — protection rules and required reviewers
- Managing your personal access tokens — classic versus fine-grained scoping
- Reviewing the audit log for your organization — event reference
- About secret scanning — including push protection
Secondary reporting on the incident, both published 2026-11-13. No primary vendor advisory was available at the time of writing:
- TechRadar, Thousands of GitHub repositories targeted in widespread attack (15:20 UTC)
- ShiftDelete.Net, GitHub Hack Uses 4,837 Stolen Accounts to Loot CI Secrets (18:19 UTC)
More posts

Auditing GitHub Actions for the wfx-loot Pattern: Workflow Triggers, OIDC Scope, and npm Execution
A technical audit guide to the wfx-loot campaign, tracing malicious GitHub Actions triggers, over-scoped OIDC cloud tokens, and npm execution paths to build detection and containment controls for CI/CD pipelines.

Detecting Outlook NTLM Credential Theft via Child Processes and Outbound SMB Auth
A technical walkthrough of the actively exploited Outlook NTLM credential-theft zero-day, covering the exploit path, suspicious Outlook child processes, outbound SMB authentication detection, and mitigation priorities beyond patching.

Gating a Bedrock Coding-Agent Router on Cost per Accepted Patch
How to evaluate NVIDIA NeMo Switchyard on Amazon Bedrock for routing coding-agent requests by task difficulty, and gate rollout on latency, pass rate, and cost per accepted patch instead of defaulting to the largest model.
Loading comments…