CSIPE

Published

- 21 min read

A Disabled GitHub Action Is Not a Revoked Dependency


Books by the author

Compare all 5

As an Amazon Associate I earn from qualifying purchases. Buying through these links costs you nothing extra and helps pay for the blog.

A maintenance workflow fails on a Tuesday because GitHub has disabled the action it calls. The repository owner sees red jobs, waits for the upstream problem to clear, and reruns the workflow when the dependency becomes reachable again. Green returns. So does the code behind the action’s old version tag.

That sequence stopped being hypothetical this month. Two third-party GitHub Actions, actions-cool/issues-helper and actions-cool/maintain-one-comment, were compromised during the Mini Shai-Hulud campaign in May 2026 and disabled. On 16 September, both repositories became accessible again while release tags still pointed to malicious code. On 25 September, GitHub disabled them a second time (Socket: re-enabled GitHub Actions; The Hacker News: compromised actions came back online).

The lasting lesson sits in the gap between those dates. A platform takedown can stop code from being fetched for a while. It does not rewrite your workflow, replace a mutable reference, revoke a stolen credential, or prove that a later restoration is clean. Availability changed. Trust did not.

My position is blunt: a third-party action referenced by a tag is executable code on a moving pointer. If that workflow can read secrets, mint cloud credentials, publish packages, or alter a repository, the pointer belongs in the same review process as a production dependency. A marketplace badge and a familiar version label do not make it fixed.

What returned on 16 September

The two actions performed ordinary repository housekeeping. One helped check or close issues. The other maintained a single comment. That is exactly why the incident matters. Teams often scrutinise deployment actions and treat issue automation as harmless plumbing, even though every action runs inside a workflow job with the permissions and data available to that job.

Socket reported that the repositories had been disabled after the May compromise. When they became reachable on 16 September, the malicious release tags had not been cleaned. Workflows that referred to those tags could therefore fetch and execute the payload again. When GitHub disabled the repositories on 25 September, those workflows began failing at job setup instead of running the code (Socket: timeline and affected actions).

Independent reporting matched the core sequence. The Hacker News named the same two actions, the same 16 September return, and the second disablement. BleepingComputer also reported that the actions remained accessible for more than a week before the second takedown (The Hacker News: September reappearance; BleepingComputer: GitHub Actions re-enabled with payload active). These reports support the public facts. They do not establish that every repository naming either action executed a malicious run during that window.

That distinction matters. A workflow file can exist without running. A job can start without receiving sensitive credentials. A repository can name a tag that resolves differently at different moments. Public dependency counts do not equal confirmed victims, and a reference in an archived branch does not prove execution. Any local response should separate presence, resolution, execution, and effect.

The first state is reference: a workflow contains uses: actions-cool/issues-helper@... or uses: actions-cool/maintain-one-comment@.... The second is resolution: GitHub fetched a particular commit for that reference. The third is execution: the affected action ran in a job. The fourth is effect: the job exposed a useful credential, sent data out, changed a repository, or produced another observable consequence.

Collapsing those states creates two bad outcomes. One team sees a reference and announces a breach it cannot prove. Another sees no obvious repository change and decides nothing happened, even though a runner may have exposed a short-lived token or environment secret. The right response keeps uncertainty visible while moving quickly on the authority that could have been reached.

As of 27 September 2026, the two repositories are disabled again according to the reporting above. That is useful containment at the platform level. It is not a local incident report for your organisation. Your workflow history, resolved action commits, token permissions, network records, secret bindings, and downstream audit logs decide what happened in your environment.

A tag looks like a version but behaves like a signpost

A workflow reference such as vendor/action@v2 feels precise. It has a product name and a version. Underneath, the tag is a Git reference that can be moved to a different commit by someone with sufficient authority in the action repository. A branch name such as main is obviously expected to move. A version tag often creates the opposite expectation even though the platform can still resolve it to different code later.

The convenience is intentional. An action maintainer can update a major tag from v2.2.0 to v2.2.1, and downstream workflows receive the repair without editing their YAML. That saves maintenance. It also hands future code selection to the upstream repository at the moment each job starts.

The September incident shows the failure in concrete terms. GitHub disabling the action broke the lookup. Restoring reachability made the old references work again. Because the affected tags still led to malicious code, the return of availability also restored the dangerous path. No downstream pull request was required. The consumer’s workflow file could remain byte-for-byte unchanged.

GitHub’s own security guidance is unusually direct here. It says pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release. A full SHA makes the workflow request one Git object rather than whatever object a tag names at run time (GitHub Docs: secure use of third-party actions).

Pinning changes an invisible upstream edit into a visible downstream change. If a maintainer releases a repair, an update tool or engineer proposes a new SHA. The diff shows the exact object changing. Review can compare source, provenance, release notes, and expected behavior before the new code receives workflow authority.

A SHA does not make bad code good. Pinning a malicious commit preserves the malicious commit perfectly. Pinning also cannot protect a workflow that checks out untrusted code into a privileged context, gives every job write access, or passes broad cloud keys to routine automation. Its claim is narrower and valuable: the reviewed code stays the code requested on the next run.

This is why a comment beside the SHA helps. Humans can read the intended release while the machine uses the immutable identity:

   - uses: owner/action@0123456789abcdef0123456789abcdef01234567 # v2.2.1

The version comment is not the control. The 40-character commit identity is. An update process should verify that a proposed SHA belongs to the expected repository and release before changing it. Copying a hash from an unauthenticated chat message simply moves trust from a tag to a stranger.

Some teams object that SHA pinning creates update work. It does. That work is the review boundary. Automate the proposal, not the decision. Dependency tools can open pull requests when a pinned action has a new release. A human or policy gate can then inspect the commit change before merging it.

The useful comparison pits automatic execution of future upstream code against a reviewable update queue. Workflows that publish, deploy, sign, or hold strong credentials should choose the queue.

The action runs with the job’s authority

The name “issue helper” encourages a mental model of a small bot tidying labels. The runner sees something different: code executing in a job. That code may read the workspace, environment, event payload, generated files, network, and the job’s GITHUB_TOKEN. Additional secrets can enter through workflow configuration. Cloud authentication may be available through long-lived keys or OpenID Connect, where the job exchanges a signed identity token for temporary credentials.

The action does not carry a private permission envelope just because it appears as one step. Steps in a job share important context. A checkout step places source in the workspace. A build step creates artifacts. A later third-party step may be able to read both. Environment variables and credentials can cross boundaries that are obvious in YAML but weak at runtime.

GitHub warns that actions can access the GITHUB_TOKEN through the github.token context even when the workflow does not pass the token as an explicit input. Its guidance recommends setting the default token permission to read-only for repository contents and raising permissions only for jobs that need them (GitHub Docs: principle of least privilege). That advice changes the consequence of a compromised step.

Consider two versions of the same housekeeping workflow. In the first, the repository default grants write permissions and the job can alter issues, contents, pull requests, and actions. A malicious action has several useful routes. In the second, the top-level workflow declares no permissions and the one housekeeping job receives only the exact issue permission required. The same untrusted code still runs, but the repository authority around it is smaller.

Secrets need the same treatment. A maintenance action that closes stale issues should not run in a job containing a package-publishing token. It should not inherit production cloud access because the organisation copied one workflow template everywhere. It should not mount an SSH key used for deployments. Convenience at the YAML level becomes incident scope at the runner level.

Short-lived credentials improve the cleanup path, but expiry is not a complete defense. A token valid for ten minutes can publish a package, alter a release, read private source, or mint another resource during those ten minutes. Short life reduces the later replay window. Narrow scope limits what can happen while the credential is alive.

Network access supplies the exit. A compromised action may collect useful values, but theft still needs a route from the runner to somewhere the attacker can receive them. Many hosted and self-hosted runners can contact arbitrary internet destinations. That makes every secret available to the job potentially exportable.

Restricting outbound traffic on CI is harder than adding a scanner, yet it produces a real boundary. Release jobs often need a short destination list: source hosting, a package registry, an artifact store, a signing service, and a deployment API. A job that only manages issues may need no arbitrary outbound access at all. If a new host appears, the denied connection becomes a useful incident signal.

Self-hosted runners add another layer. They may reach internal services that GitHub-hosted runners cannot. They may persist workspaces, caches, credentials, or tools between jobs. A compromised action on a long-lived runner can therefore touch more than the repository described by the workflow file. Ephemeral runners, segmented networks, clean images, and one-job lifetimes reduce that shared residue.

Risk review should measure the action code multiplied by the job’s reachable authority, rather than the action name. A simple action in a privileged release job deserves more scrutiny than a complex test action inside a disposable, read-only, network-limited runner.

A failed dependency is not a revoked dependency

When GitHub disabled the two repositories in May, downstream jobs stopped fetching them. That changed the platform’s response to a lookup. It did not edit consumers’ workflow files. The stale references remained, waiting for a future run.

This resembles a recalled component left in a deployment manifest. The warehouse refuses to ship it today, so the build fails. If the warehouse later accepts the same product code again, the manifest resumes ordering it. The temporary refusal never removed the dependency from your system of record.

A genuine revocation process needs local state. The organisation marks the action identity or commit as forbidden, searches every default branch and relevant historical release branch, blocks future use through policy, and records which workflows require replacement. Restoring upstream availability cannot silently reverse that decision.

That local deny decision should include more than an action name. Names can be transferred or imitated. Record the repository owner, repository identity where available, disallowed tags and commits, discovery date, reason, affected workflows, replacement, and review owner. The record becomes an input to policy checks and future investigations.

Repository rules can help. GitHub Actions settings let organisations restrict which actions and reusable workflows may run, including limiting use to selected sources. GitHub also supports policy enforcement around full-SHA references (GitHub Changelog: Actions policy for SHA pinning). A central rule turns one incident lesson into a fleet control instead of relying on each maintainer to remember the names.

Allowlisting by publisher alone is insufficient. A trusted maintainer can lose an account, a repository can be compromised, and an approved action can later change behavior. Allowlisting and pinning answer different questions. The allowlist controls who may supply code. The SHA controls which exact code may run.

Forking every third-party action into an internal organisation is also incomplete. A fork gives you custody of the copy and a place to review updates. It can still drift, collect stale vulnerabilities, or grant code more permissions than needed. Internal ownership shifts maintenance responsibility rather than erasing it.

The same caution applies to marketplace verification and popularity. Those signals can help with initial selection. They do not make every future commit safe, and they do not bind a mutable tag. An action with thousands of dependents can spread a bad update more efficiently than an obscure one.

A local dependency ledger closes the recall gap. For each external action, record the workflow, pinned SHA, human-readable release, job permissions, secrets or identity providers present, runner type, and update owner. Generate as much of the ledger as possible from workflow files and repository settings. Keep the few facts that require judgment, such as why the action is approved, beside the generated inventory.

Then test removal. Choose one approved action and mark its current commit forbidden in a staging policy. The next workflow should fail with a message that names the dependency and replacement path. If the only alert is an upstream 404, the organisation still depends on someone else’s availability decision as its revocation system.

Work out whether the September window reached you

Start with source, but do not stop there. Search workflow files on default branches, release branches, tags used for production, reusable workflow repositories, templates, and generated pipelines for both affected action names. Include local composite actions that may call them indirectly.

Record the exact reference. A full SHA, a release tag, a major tag, and a branch describe different resolution behavior. Keep the repository, workflow path, job name, trigger, and dates when the reference was present. A current search cannot find a dependency removed on 26 September after it ran on 20 September.

Workflow history supplies the next layer. Identify runs from 16 September until the second disablement on 25 September 2026. Match each run to the workflow commit that GitHub used, not merely today’s file. Record whether the affected job started, failed during setup, completed, or was cancelled. Preserve logs and run metadata according to your incident policy before retention removes them.

The action’s resolved commit is critical evidence. Where logs, workflow metadata, dependency submission records, runner telemetry, or cached action directories expose it, record the full SHA actually fetched. Compare it with the malicious commits and tags published by the researchers. A tag written in YAML cannot answer what it resolved to on a past date by itself.

Classify authority for every executed run. Capture the declared permissions block and the repository’s default token setting at that time. List environment secrets released to the job, repository and organisation secrets in scope, cloud roles available through OpenID Connect, package registry credentials, deployment environments, signing services, and internal routes available from the runner.

Do not assume a secret was safe because the action input did not name it. Review the whole job and runner context. GitHub’s guidance notes that actions may access the token from context, while other credentials may exist in environment variables, files created by earlier steps, credential helpers, or cloud metadata paths (GitHub Docs: auditing secrets and third-party actions).

Network and identity logs can then show effect. Search proxy, Domain Name System, firewall, endpoint, and cloud records for first-seen destinations from the affected runners. Review GitHub audit events for unexpected pushes, workflow edits, deploy keys, applications, releases, or token use. Check package registries for publications and cloud providers for sessions or API calls linked to the job identity.

A quiet result needs a measured sentence. “No affected run had write permission or production secrets, and retained network logs show no unknown destinations” is useful. “We were not compromised” may exceed the evidence if runner logs expired or outbound traffic was not recorded.

If an affected job had meaningful authority, rotate or revoke credentials from a trusted administrative environment. Review actions taken by those identities during and after the exposure window. Rebuilding a runner does not invalidate a copied token. Deleting the workflow reference does not undo a package publication.

Self-hosted runners deserve a clean rebuild when the affected action executed and the host carried useful credentials or internal access. Investigation may preserve an image first, depending on policy and impact. After evidence collection, replace the runner from a trusted image rather than cleaning one suspicious folder and returning the machine to service.

Artifacts also need lineage review. A compromised job may have produced a package, container, binary, deployment manifest, or cache consumed later by another trusted workflow. Identify outputs from affected runs, where they moved, and whether they were published or deployed. Rebuild high-impact artifacts on a clean runner from reviewed source and pinned dependencies.

Do not rerun an old failed workflow after changing only the upstream availability. A rerun can use the old workflow definition and recreate the same dependency choice. Make the source change, verify the pin or replacement, narrow permissions, then start a new run whose inputs and workflow commit are recorded.

Build the workflow receipt

A green check mark proves that the workflow reached a successful exit state. It does not prove which future code a tag will select, what authority surrounded each step, or where the resulting artifact went. A workflow receipt should answer those questions after the runner has disappeared.

The first field is the workflow identity: repository, workflow path, source commit, event type, run ID, attempt number, and reusable workflow commits. The second is the executable dependency set: every external action and reusable workflow resolved to a full commit SHA. Store the friendly release label as annotation, not identity.

The third field is authority. Record effective GITHUB_TOKEN permissions by job, environment approvals, secrets released, cloud role and session identity, runner class, mounts, and reachable network policy. Configuration intent is useful, but effective values matter. A repository default can change the meaning of a workflow that omits permissions.

The fourth field is output. Record artifact names and digests, package or container coordinates, attestations, deployment target, and final status. Join that output to the exact run and source commit. If an incident affects one action SHA, the team should be able to list every artifact created by runs that used it.

Keep the receipt outside the job’s writable control. A compromised action should not be able to rewrite the only record of which action ran. GitHub’s run metadata, organisation audit log, external identity provider logs, registry records, and an independent security log can supply overlapping evidence.

The receipt should survive mutable labels. If a tag moves next week, yesterday’s record still names yesterday’s commit. If a repository disappears, the record still shows what was executed. If a package is rebuilt, the old artifact digest remains linked to the suspect run.

This does not require a large platform project. Start by exporting run metadata and parsing workflow references. Require full SHAs in policy. Send cloud identity and outbound connection logs to central storage. Add artifact digests from the build system. A small joined record beats a glossy inventory that cannot connect dependencies to runs.

Test the receipt with a real question: “Which production artifacts were built between 16 and 25 September by jobs that used either affected action, and what credentials could those jobs obtain?” Give the question to someone outside the workflow’s owning team. If the answer takes two days of screenshots and guesses, the evidence path needs work.

The Secure Harness frames coding-agent authority as something that must be bounded and recorded. CI automation deserves the same treatment. It is autonomous code acting with tools and credentials in response to an event. Familiar YAML does not make the execution less consequential.

What to change this week

The immediate work should produce a smaller attack surface and a durable record. Avoid a broad workflow rewrite while evidence is still being collected. Make narrow, reviewable changes in an order that preserves what happened and prevents a repeat.

  1. Search and preserve. Find current and historical references to actions-cool/issues-helper and actions-cool/maintain-one-comment. Preserve workflow commits, runs, logs, resolved SHAs, artifacts, permission settings, secret bindings, and runner records for 16-25 September 2026.

  2. Classify each run by authority. Separate a dormant reference from a completed job. For completed jobs, list token permissions, environment secrets, cloud roles, package rights, runner type, internal access, and artifacts produced. Write down evidence gaps instead of treating missing logs as a clean result.

  3. Remove or replace the affected actions. Do not wait for another upstream status change. If the function is still needed, choose a reviewed replacement or implement the small housekeeping behavior locally. Pin any third-party replacement to a verified full commit SHA.

  4. Enforce immutable references. Add an organisation or repository policy that rejects third-party actions not pinned to full SHAs. Let an update service propose new commits through pull requests. Require source and release review for privileged workflows before merging.

  5. Reduce job authority. Set a read-only or empty top-level permissions block, then grant each job only what it needs. Move publishing, signing, and deployment into separate jobs protected by environments and review. Remove secrets from maintenance jobs.

  6. Bound the runner. Prefer ephemeral runners for sensitive jobs. Segment self-hosted runners by trust and purpose. Restrict outbound destinations where the workflow has a predictable network path. Keep runner, identity, and network logs beyond the time it usually takes a supply-chain incident to surface.

  7. Create the receipt. Store workflow commit, resolved action SHAs, effective permissions, issued identity, artifact digests, and deployment result under the run ID. Rehearse the query across one release pipeline, then expand to the workflows with the strongest authority.

These steps reinforce each other. Pinning freezes code selection. Permission limits shrink consequence. Network policy narrows the exit. Ephemeral runners remove residue. Receipts show which artifacts and identities require attention when an upstream project fails.

Do not begin by banning all third-party actions. Teams will copy code into opaque local scripts and lose update visibility. A better default is explicit approval, immutable identity, narrow authority, and a named owner for updates. Exceptions should be rare, documented, and kept away from release credentials.

Do not rely on code review alone either. A reviewer can inspect the workflow line that says @v2 and still miss that the object behind it may change after approval. Review works when the reviewed identity is the executed identity.

The same principle applies to reusable workflows. Referencing a reusable workflow by a branch or mutable tag hands future orchestration to upstream state. Pin the exact commit, review the called workflow’s permissions and secret interface, and include its SHA in the receipt.

Finally, make recalls local. Maintain a denylist or advisory feed that your policy checks before execution. When a dependency is disabled upstream, open tracked replacements immediately. A platform restoration should never be able to re-authorise code your organisation already rejected.

The trust decision belongs in your repository

The September reappearance was short, but the control failure can last for years. A workflow line with a mutable tag can outlive its original reviewer, the action maintainer, the security incident, and the platform takedown. Every future run asks the upstream repository what code that old decision means today.

GitHub’s second disablement stopped the two named actions again. That response protects the platform while the block remains. It cannot tell a downstream team which runs executed, which secrets were present, which artifacts were built, or whether a restored repository should ever be trusted again.

Those are local questions. Answer them with a full SHA, least-privilege job, bounded runner, and independent run receipt. Remove known-bad references from source instead of treating failure as revocation. Keep the update path visible in pull requests.

A tag is useful release metadata. It is a weak execution identity. The moment a workflow can publish, deploy, sign, or hold valuable credentials, weak identity becomes borrowed authority.

The practical rule is small enough to remember: pin the code, narrow the job, keep the receipt. Then an upstream repository can disappear, return, or change hands without silently redrawing the trust boundary inside your pipeline.

For one practical security and AI engineering note each month, the newsletter sends one email per month. The signup is on this site.

Sources