actions-cool/issues-helper compromised
· 1825 words · 9 minutes read
Supply Chain Compromise: actions-cool/issues-helper (All Tags)
TL;DR
All tags for actions-cool/issues-helper have been compromised. Every tag was re-pointed to malicious commits authored on 2026-05-18. There is no safe tag to pin to. The injected code reads decrypted secrets directly from runner process memory, harvests GitHub Actions OIDC tokens, AWS credentials, NPM tokens, and more, then exfiltrates everything to t.m-kosche.com. It also injects persistent malicious hooks into Claude Code and VS Code configurations on the runner. Remove the action from all workflows immediately or pin to the last known-safe SHA for your version.
External analysis: StepSecurity published a concurrent investigation: actions-cool/issues-helper GitHub Action Compromised — All Tags Point to Imposter Commit That Exfiltrates CI/CD Credentials
Introduction
This report documents a confirmed active supply-chain compromise affecting actions-cool/issues-helper. Each tag was re-pointed to its own distinct malicious imposter commit, authored by lijinke666. All tags in the repository point to malicious imposter commits — there is no safe version tag. Any workflow referencing actions-cool/issues-helper at any tag that executed after 2026-05-18T19:11:08Z should be treated as fully compromised.
This is an active incident. Details may evolve as forensic analysis continues.
Timeline
- 2026-05-18T19:11:08Z - Coordinated tag drift begins. All tags were re-pointed to distinct malicious imposter commits within a 10-minute window, consistent with scripted, automated execution.
- 2026-05-19T19:39:00Z - I post a supply chain compromise report to the GitHub repo issues, https://github.com/actions-cool/issues-helper/issues/230.
- 2026-05-19T22:42:00Z - Consistent with active attacker suppression, lijinke666 closes the issue and cleared out the report, locking it to prevent it being reopened.
- 2026-05-19T23:26:00Z - I created a new issue to report the attack, https://github.com/actions-cool/issues-helper/issues/231, and I am monitoring for it to be reclosed. I am under the assumption the credentials have enough access to close issues, edit the issues, but not necessarily block users.
- 2026-05-19T23:43:00Z - On or around this time GitHub has taken down the repository.
Attack Analysis
Execution Model Change
The first and most operationally significant change is in action.yml. The original action used a direct Node.js execution model:
runs:
using: node16
main: 'dist/index.js'
The malicious commit replaces this with a composite action that installs the Bun runtime and executes a newly added, opaque 500 KB index.js:
runs:
using: "composite"
steps:
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
- name: Run script from action's directory
shell: bash
run: bun run $GITHUB_ACTION_PATH/index.js
- uses: actions-cool/issues-helper@200c78641dbf33838311e5a1e0c31bbdb92d7cf0
with:
actions: ${{ inputs.actions }}
token: ${{ inputs.token }}
The third step delegates to the same action at a hardcoded SHA. This two-stage design allows the malicious index.js to execute first — harvesting credentials — while the action continues to perform its legitimate issue-management function in the second stage. This significantly reduces the likelihood that affected users notice any behavioral change.
If bun is not present on the runner, the payload installs it silently before executing:
command -v bun >/dev/null 2>&1 || (curl -fsSL https://bun.sh/install | bash && export PATH=$HOME/.bun/bin:$PATH)
Complete Source Replacement
All 68 original source files were deleted: the full TypeScript source tree (src/), tests, build scripts, CI workflows, documentation, and the prior compiled bundle (dist/index.js). The sole addition is the new 500 KB index.js. Deleting the source eliminates the auditable artifact and removes prior compiled bundles that security scanners may have already profiled.
Runner Memory Scraping
The most dangerous capability in the payload is direct extraction of secrets from runner process memory. The malware spawns python3 to read /proc/<PID>/mem — the address space of the Runner.Worker process — which holds all decrypted workflow secrets at runtime:
(Representative snippet, reconstructed from static analysis of the obfuscated index.js)
# Reconstructed from static analysis of index.js
import sys, re
with open(f'/proc/{pid}/mem', 'rb') as mem:
# Read address ranges from /proc/<pid>/maps
# Filter extracted content for isSecret:true entries
This technique bypasses secret masking entirely. Every secret passed to the workflow — regardless of name — is exposed in the Runner.Worker address space and is therefore recoverable by this method. No static secret rotation policy is sufficient against this attack if a run was executed while the malicious code was present.
OIDC Token Harvesting
The malicious index.js directly reads ACTIONS_ID_TOKEN_REQUEST_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL from the runner environment:
// Extracted from static analysis
let { ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL } = process.env;
const response = await fetch(ACTIONS_ID_TOKEN_REQUEST_URL + '&audience=...');
const { token } = await response.json();
// token is then exfiltrated
This short-lived OIDC token can be exchanged for cloud-provider credentials — AWS IAM roles via sts.amazonaws.com, GCP service accounts, Azure managed identities — without requiring any static secrets to be present in the repository.
Broad Credential Scraping
Beyond memory and OIDC, the payload scans for a wide range of credential types using embedded regex patterns. Confirmed targets found in static analysis:
| Credential Type | Method |
|---|---|
| GitHub tokens | process.env.GITHUB_TOKEN, regex on env |
| AWS access keys | process.env.AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
| AWS Secrets Manager | secretsmanager:GetSecretValue, secretsmanager:ListSecrets |
| AWS ECS metadata | HTTP fetch to http://169.254.170.2 |
| NPM tokens | https://registry.npmjs.org/-/npm/v1/tokens |
| HashiCorp Vault | Read from /etc/vault/token |
| Generic secrets | Regex: /(password|passwd|pass|pwd|secret|token|key|api[_-]?key|auth)\s*[:=]\s*["']?[^\s"']+/i |
| Base64 blobs | Regex: /[A-Za-z0-9+\/=]{40,}/g |
| DB connection strings | Regex: /(mongodb|mysql|postgresql|postgres|redis):\/\/[^:\s]+:[^@\s]+@[^\s'"]+/gi |
| Stripe keys | Regex: /(sk|pk)_(test|live)_[0-9a-zA-Z]{24,}/g |
| Slack tokens | Regex: /xox[baprs]-[0-9a-zA-Z\-]{10,}/g |
| URL-embedded credentials | Regex: /https?:\/\/[^:"'\s]+:[^@"'\s]+@[^\s'"\]]+/g |
Exfiltration
Harvested credentials are POSTed to t.m-kosche.com (confirmed by StepSecurity). The payload uses two AES-encrypted C2 URL variables (u4 and g4) whose ciphertext is embedded in the obfuscated bundle. The AES key and PBKDF2 salt are also embedded:
Key (PBKDF2 input): 86c1aaef3b3b1f82b954385fb31cbee557a1464a6104de004d8ba2b93f95f253
Salt: c87c0ebc2a2558ebde23d501ee2144df
u4 ciphertext: rHQkwQ/ptOCFFMOSzs9N1ZUomT5WrCtXdh7c5oI/16VOSZPvMmg=
g4 ciphertext: LDzpeps/HPp8+OtsZ8f6pdpXXFeEw5BX4VijHN659ayDsIRcdrI=
The use of f14782086() — a globally-registered AES decrypt function — to store all sensitive strings (environment variable names, endpoints, API paths) means no plaintext indicators appear in the bundle without decryption.
Additionally, the payload uses an embedded PEM private key consistent with asymmetric encryption of exfiltrated data before transmission — ensuring only the attacker holding the corresponding private key can decrypt the stolen payload.
Claude Code Hook Injection
A capability not commonly seen in supply-chain attacks of this type: the payload injects malicious hooks into Claude Code and VS Code configurations on the runner. Affected files:
.claude/setup.mjs— malicious Bun script written to the runner workspace.vscode/setup.mjs— identical pattern targeting VS Code users.claude/settings.json— modified to register persistent hooks
The hooks are registered via addHook and installTokenMonitor functions found in the payload. The intended effect is that any subsequent bun run .claude/ or Claude Code session on the same machine would re-execute the credential harvester without a GitHub Actions context. This is a persistence mechanism: the compromised runner or developer machine continues exfiltrating credentials after the workflow completes.
Relevant decoded strings from static analysis:
bun run .claude/
initialize
installTokenMonitor
addHook
.claude/setup.mjs
.claude/settings.json
.vscode/setup.mjs
~/.claude/package/
GitHub Write-Back
Using the stolen GITHUB_TOKEN, the payload can commit code to repository branches via the GitHub GraphQL API:
CreateCommitOnBranchInput! (GraphQL mutation found in payload)
/branches?per_page=30 (branch enumeration endpoint)
This enables the attacker to introduce backdoor code into target repositories as a secondary persistence vector, independent of the runner machine.
Coordinated Tag Drift
All tags in the repository were moved to the malicious commit within a 10-minute window. This is the signature of a scripted compromise designed to maximize blast radius — no tag reference is safe. Any consumer pinning to any version tag, received the malicious payload.
Indicators of Compromise
| IOC | Type |
|---|---|
t.m-kosche.com |
C2 exfiltration domain |
| Imposter commits on all tags (each tag has a distinct SHA) | Malicious commit SHAs |
.claude/setup.mjs present in workspace |
Persistence file |
.vscode/setup.mjs present in workspace |
Persistence file |
.claude/settings.json modified with unexpected hooks |
Persistence |
~/.claude/package/ populated unexpectedly |
Persistence |
c87c0ebc2a2558ebde23d501ee2144df |
Embedded salt / marker hash |
setup-bun step appearing in workflows that did not install it |
Execution indicator |
| Outbound HTTPS POST to unexpected domain during workflow | Network IOC |
Mitigation
1. Pin to the Last Known-Safe SHA
All tags are compromised — there is no safe tag. Remove the action from all workflows entirely, or if the action’s functionality is required, pin explicitly to the last known-safe commit SHA:
# UNSAFE — every tag points to the malicious commit
- uses: actions-cool/issues-helper@v3.5.1
# Safe — pinned to last known-safe commit SHA
- uses: actions-cool/issues-helper@25379ae1ea683ac484497214db131ab8f003a52b
Prefer removing the action entirely until the repository owner publicly confirms full remediation and resets all tags to verified commits.
2. Rotate Compromised Credentials — In Order of Urgency
- OIDC-federated cloud credentials: Rotate all AWS IAM roles, GCP service accounts, and Azure managed identities that were available via OIDC federation on any runner that executed the compromised action after 2026-05-18T19:11:08Z. The harvested
ACTIONS_ID_TOKEN_REQUEST_TOKENcan be exchanged for temporary cloud credentials; active sessions may still exist. - Repository secrets: Rotate all values stored as
secrets.*that were accessible to workflows invoking the compromised action. The runner memory scraping technique reads decrypted secrets directly from the Runner.Worker process — treat every secret mounted in the runner environment during affected runs as exposed regardless of masking. - AWS credentials: Rotate
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY. Audit AWS Secrets Manager access logs for unexpectedGetSecretValueandListSecretscalls originating from runner IPs after 2026-05-18T19:11:08Z. - NPM tokens: Audit
https://registry.npmjs.org/-/npm/v1/tokensfor any tokens created or used unexpectedly. Rotate any NPM automation or publish tokens that were present in affected runner environments. - HashiCorp Vault tokens: Rotate any Vault tokens. The payload reads
/etc/vault/tokendirectly. Audit Vault audit logs for unexpected secret reads from runner IPs. GITHUB_TOKEN-derived access: WhileGITHUB_TOKENis short-lived, any Personal Access Tokens or long-lived tokens present in the runner environment of affected runs should be rotated immediately. Audit all repositories that ran the compromised action for unexpected new webhooks, deploy keys, collaborator additions, or commits from unknown authors — the payload can commit to branches using the stolen token.
3. Check for Persistence Artifacts
On any self-hosted runner or developer machine where workflows using the compromised action were run, inspect for:
# Persistence files written by the malware
ls -la .claude/setup.mjs
ls -la .vscode/setup.mjs
ls -la ~/.claude/package/
# Unexpected hook entries in Claude Code settings
cat .claude/settings.json
Remove any unexpected files. If .claude/settings.json contains hook entries pointing to setup.mjs or referencing bun, treat the configuration as compromised and restore from a known-good backup.
4. Audit Workflow Run Logs
Review GitHub Actions run history for all repositories using any of the 14 drifted tags. Examine every run that executed after 2026-05-18T19:11:08Z.
In each run’s log, search for:
- Any output referencing
ACTIONS_ID_TOKEN_REQUEST_TOKENorACTIONS_ID_TOKEN_REQUEST_URL - Unexpected
curl,fetch, orbunnetwork calls in the step logs - The presence of
setup-bunin the step list for a workflow that did not explicitly install Bun — this is a reliable indicator the malicious composite action executed - Unexpected calls to
sts.amazonaws.com,oauth2.googleapis.com, ormanagement.azure.comin runner network logs if outbound traffic logging is enabled - Outbound connections to
t.m-kosche.com
Treat the runner environment of every affected run as fully compromised: all environment variables, mounted secrets, and OIDC-exchangeable tokens from those runs should be considered exposed.
5. Harden Remaining Workflows
Pin all third-party GitHub Actions to full 40-character commit SHAs rather than mutable tags. Consider deploying StepSecurity Harden-Runner or an equivalent tool to detect and block unexpected outbound network connections at runtime.
Conclusion
This is an ongoing investigation. I will update this report as new information emerges. If you have any questions or additional insights, feel free to reach out to me via Linkedin.