Programming guides for beginner...
Any comments are welcomed....
I hope it helps!!! Thanks for drop by...

Tuesday, June 16, 2026

The Recruiter's Repo. The npm install Was the Backdoor.

The Recruiter's Repo. The npm install Was the Backdoor.

On 15 June 2026, Roman Imankulov published a post-mortem on his own blog at roman.pt describing the most disquieting recruitment-trail attack of the year. A recruiter claiming to represent a "small crypto startup" messaged him on LinkedIn, ran him through a normal-feeling multi-day conversation, then sent a public GitHub repo and asked him to "check out the deprecated Node modules issue." The repo contained a package.json whose prepare script ran node app/index.js, an app/index.js whose very first line was require('./test'), and an app/test/index.js whose ~250 lines hid a URL-assembly routine that built https://rest-icon-handler.store/icons/77 from string fragments and then "ran anything the server sent back to your machine." The post hit Hacker News as item 48546294 and was sitting at 568 points and 109 comments the morning of 16 June 2026. It is not the technical novelty that is new. The novelty is that the delivery vehicle is the hiring funnel, and that an AI coding agent in read-only mode is what caught it.

The attack in one paragraph

The trap is laid in three pieces of code, all in the same repo. package.json declares a prepare script — prepare is a documented npm lifecycle hook that runs automatically after npm install from a local path, a git URL, or a tarball. The script chain is prepareapp:prenode app/index.js. The app/index.js entry point does const test = require('./test') at the top level, which loads the test file as a side effect of being required. And app/test/index.js, disguised as a test suite with "walls of commented-out tests," assembles its C2 endpoint from string fragments — protocol = "https", domain = "store", subdomain = "rest-icon-handler", path = "/icons/", token = "77", etc. — then evaluates whatever the server returns. The deobfuscation step is the part Imankulov did not run; he read enough of the source to stop. The point is that none of the three pieces is, on its own, a flag. A prepare script in a Node project is ordinary. A require of a test module is ordinary. A test file with string concatenation is ordinary. The combination, mounted on the social-engineering rails of a recruiter DMed at you by name, is what is new.

The recruiter's LinkedIn profile belonged to a real arts journalist with no technical background, and the 39 commits in the repo were attributed to a real full-stack developer whose name and email had been used on the platform before — that developer confirmed to Imankulov he had been impersonated on GitHub prior to this incident. The same recruiter DMs land in dev inboxes every week. The campaign Imankulov was targeted by is not a one-off; it is the working shape of a class.

LinkedIn has become the new phishing email — with a better pretext

The Register's 31 March 2026 write-up of an axios compromise is the same shape from a different direction: attackers compromised the npm account of jasonsaayman, the axios primary maintainer, by swapping the account's email for an anonymous ProtonMail inbox and pushing infected packages manually (bypassing the GitHub Actions CI pipeline). The published payload versions were axios@1.14.1 and axios@0.30.4, with a plain-crypto-js@4.2.1 dependency added to drop a cross-platform RAT. The Register's 23 April 2026 write-up of the Boris Vujičić / Genusix Labs incident adds higher-fidelity detail on the same chain: a camera-on Zoom interview, a "live-coding test" that delivers a patch[.]sh shell script under a camera-driver pretext, architecture detection, a Go-based backdoor with custom RC4-encrypted C2, persistence on boot, Chrome password extraction, Keychain exfil, crypto-wallet targeting. The three stories, in chronological order, sketch the same campaign moving up the stack: own the recruiter, own the maintainer, own the package. The 15 June post is the case where the recruiter is the whole attack.

The framing for security teams is: your hiring funnel is now a malware delivery channel, and the threat model that scoped supply-chain risk to "third-party npm packages we audit with Socket / Snyk / npm audit" does not see it. The candidate-side failure mode is npm install && node app/index.js against a repo the candidate has no reason to distrust, in a context where the recruiter is pressuring the candidate to move fast. The employer-side failure mode is "we trust our own recruiters" — which is correct, but is not the trust boundary that matters. The trust boundary is: a candidate will, under reasonable time pressure, npm install whatever a stranger on LinkedIn sends them, and the npm ecosystem's default prepare-script behavior is to make that npm install execute attacker-controlled JavaScript.

The AI-coding-agent angle is the one nobody else is making

The most useful sentence in Imankulov's post is buried halfway through. He notes that running the suspicious repo through an AI coding agent (Pi, in his case) with read-only tools flagged the backdoor in seconds — faster than he could have read it himself, faster than he would have caught it by skimming. This is the defensive force-multiplier that the supply-chain discourse has been under-using. The agent is not a security product. It is, however, a code reviewer that will read every line of every file the candidate was about to run, on demand, in a sandbox, with the recruiter's pressure removed. The "Pi in read-only mode" pattern is the model: any agent that can be given a directory and instructed to summarize what each file does — without executing it, without following imports, without network — collapses the candidate's review time from "however long it takes to read 250 lines" to "however long it takes to read the agent's summary." For candidates being targeted by a LinkedIn-recruiter attack, that is the difference between catching the trap and walking into it.

The second-order angle, which is the one HN's top comment thread is starting to make: this is what an honest AI-coding-agent-assisted security review feels like in 2026. The agent did not "catch malware" in any deep semantic sense. It read the file, summarized what the file did, and the human said "ah, no." That is the realistic ceiling for the agent — a fast, thorough, deterministic first pass, with the human judgment applied to the summary. The agent is the lint, not the auditor. The post should not oversell the role. But the role is real, and the 15 June story is the most widely-cited recent incident where the agent was the reason the backdoor did not run.

The npm prepare footgun is the underlying bug

The mechanism that makes this attack work is npm install executing arbitrary JavaScript out of prepare, preinstall, install, and postinstall scripts. npm's lifecycle documentation describes the behavior; the design has been the same since the early days of the package manager. The flag npm install --ignore-scripts is the opt-out, and it is not the default. The community has known about this for years — the 2018 eslint-scope postmortem, the 2018 event-stream compromise, the 2022 node-ipc / peacenotwar incident, the 2022 colors.js / faker.js maintainer compromise all rode the same lifecycle hooks. (Citations for the four historical incidents are in ## Sources; three of the canonical postmortem URLs were returning 404 as of 16 June 2026 and have been dropped from the body so the post does not carry broken links — the event-stream GitHub issue is the one surviving verified link.) The LinkedIn-recruiter story is a new delivery vehicle for an old footgun.

The 2026-era defensive posture is well-known and not yet standard. npm install --ignore-scripts for the first run on any untrusted repo is the opt-out; turning it on by default in your project's .npmrc is the cheap mitigation. None of the major package managers disable scripts by default — the default is "run whatever the package says." The LinkedIn-recruiter backdoor is a useful forcing function to make that default the wrong default at your team: if your hiring process ever asks a candidate to npm install an evaluation repo, the right policy is --ignore-scripts (or a pre-built sandbox image) and the cost of switching is one config line.

The "report and pray" gap is the systemic problem

The most-quoted HN comment on the 15 June post, from @pants2: "LinkedIn offers no way for $company to disavow users who claim to work for $company." That is the part the post is honest about. Imankulov reported the repo to GitHub and the recruiter to LinkedIn. As of the post's writing, the code was still up; the impersonated developer's complaint was filed. The Vujičić incident in April 2026 followed the same report-and-pray arc, with Vujičić reporting the fake-company repo to npm and GitHub, the Genusix profiles to LinkedIn, the domain to HostGator, and the IP to AbuseIPDB. An HN commenter on the 15 June thread linked to Microsoft's reportfraud.microsoft.com page as a model to copy; the existence of a dedicated abuse-reporting surface with a public response expectation is the part worth noting, even if the specific SLA is not documented in the thread.

The platforms have a reporting workflow and a takedown SLA they publish; the SLA is not the gap. The gap is that the report-to-action pipeline for recruiter-shaped attacks — where the malicious actor is impersonating an employer and using a public repo as the C2 trigger — does not have a category, so the report sits in the generic abuse queue while the attack keeps running.

The original take: the vulnerability is in the hiring funnel, not in npm

The defensible original framing, which the post itself does not quite make: the LinkedIn-recruiter backdoor is a vulnerability in the hiring funnel, not in the package manager. The npm prepare footgun is a known quantity. The recruitment delivery vehicle is the new thing. The threat model that catches it sits at the recruiter / HR layer, not the developer layer, and most security teams do not have a "how are candidates being asked to install code from us" review in their threat model at all.

The right fixes are at the recruiter layer. Companies that run live-coding take-homes should publish a pre-built sandbox image (Devbox, GitHub Codespaces, Daytona, or a docker compose up) and tell candidates explicitly in writing that they are not expected to clone-and-install the company's repo. Recruiters should be trained to never send a candidate a public GitHub repo to clone and run, and the training should be measured — the next incident is a training-failure metric, not a security-team incident. The candidate-facing message should be the inverse of the LinkedIn recruiter's pressure: slow down, do not install, ask for a sandbox. The candidate-side defensive posture — pnpm, --ignore-scripts, an AI-coding-agent first pass — is the bottom of the stack, and it should be on. The recruiter-side fix is the top of the stack, and it is the part the security industry has been leaving to HR.

The procurement framing: the cost of this attack succeeding is not "the candidate's laptop got owned" — that is a Tuesday. The cost is that the recruiter, the company, and the platform each have plausible deniability, and the candidate bears the loss. The fix is to make the process the attack surface, and to put the security review on the process before the recruiter DMs the next candidate. The threat model your security review still uses is the threat model that misses this.

What this means for you

  • If you are a candidate being asked to "check out our repo" by a recruiter — do not npm install it. Read the source in a read-only AI agent, or in less, or in a throwaway Hetzner box. The recruiter's pressure is the attack. Slow down; that is the defense. If the company will not give you a sandbox image, the company is the wrong company.
  • If you are a recruiter sending a take-home to a candidate — switch to a pre-built sandbox image (Devbox, GitHub Codespaces, Daytona) and document it in the take-home brief. The cost of the switch is one config file. The cost of not switching is that the next "I cloned your repo" incident is your company's name on the post.
  • If you maintain a Node project and accept outside contributions / outside test runs — set ignore-scripts=true in .npmrc for the test environment. The flag exists, the flag is one line, and the flag is the difference between "the candidate ran our CI in a clean environment" and "the candidate's laptop got owned by a prepare script we shipped in 2023 and forgot about."
  • If you run a security team — add "how are candidates being asked to install our code" to the threat-model review. The npm-audit / Snyk / Socket posture does not catch this; the threat is upstream of the install, in the recruiter-channel, and the right defensive surface is the process, not the package.
  • If you write or maintain a code-review agent — the "read-only first pass" is the right shape. The agent that catches this backdoor is the agent that reads the file, summarizes the suspicious lines, and stops. The agent that catches it by running the file is the agent that is now part of the C2 chain.

What to do this week

# 1. Set the default for any non-production install on your machine.
#    This is the single most useful one-line change you can make
#    today, and it is the bottom of the stack that catches the
#    LinkedIn-recruiter backdoor before any other defense fires.
echo 'ignore-scripts=true' >> ~/.npmrc
#    Verify with: npm config get ignore-scripts
#    This does not change anything for projects that depend on a
#    prepare/postinstall step to build (some still do); for those,
#    use --ignore-scripts=false on the one install that needs it.

# 2. If you maintain a take-home, switch the candidate-facing
#    instructions to a sandbox image. The minimum viable version:
#    a Dockerfile that pins the Node version, copies the repo,
#    and runs the test command. The candidate runs:
#       docker compose up
#    instead of:
#       git clone <url> && cd <repo> && npm install
#    The Hetzner + Pi + read-only-tools pattern from Imankulov's
#    post is the same idea, lower-fidelity, single-use. Use it.

# 3. If you are evaluating an AI-coding-agent's security-review
#    value, the right test is: point it at a public repo you
#    do not know, ask it to summarize every file in the repo
#    and flag anything that looks like a lifecycle-script exploit,
#    a require-chain that loads an unexpected file, or a URL
#    constructed from string fragments. The agent that catches
#    the synthetic version of the 15 June trap in under a
#    minute is the agent that catches the real one. The agent
#    that does not is the agent you do not want as your
#    first-pass reviewer.

# 4. If you are a security lead, file a Jira / Linear ticket
#    titled "hiring-funnel threat model" with a single line:
#    "How are candidates being asked to install code from us?"
#    The next recruiter-shaped attack is a question of when, not
#    if. The ticket is the audit trail that the question was
#    asked. The answer is the policy that catches the next one.

# 5. Read the Imankulov post end to end. The technical walkthrough
#    is short; the social-engineering context is the part that
#    will change how you read recruiter DMs for the next quarter.
#    The Wayback Machine has the canonical copy at:
#    https://web.archive.org/web/20260615230051/https://roman.pt/posts/linkedin-backdoor/
#    (the live page was last-modified 2026-06-15 20:28:55 UTC,
#    ~28 minutes after the HN submission went live; the
#    Wayback snapshot from 20260615230051 has the correct content)

Related reads from this blog

Disclosure

Disclosure: Drafted with AI assistance. Primary source: Roman Imankulov, "A backdoor in a LinkedIn job offer," https://roman.pt/posts/linkedin-backdoor/, published 15 June 2026 (last-modified 2026-06-15 20:28:55 UTC, verified via curl -I); a Wayback Machine snapshot is retained at https://web.archive.org/web/20260615230051/https://roman.pt/posts/linkedin-backdoor/ for readers hitting the page in a state of flux. HN thread: item 48546294, submitted by @lwhsiao on 15 June 2026, 568 points and 109 comments as of 16 June 2026 08:00 UTC+8 (counts moving; fact-check pass retrieved 568 points on 16 June 2026 00:17 UTC). Secondary sources: The Register, "Top npm package backdoored to drop dirty RAT on dev machines" (axios jasonsaayman account compromise via email swap, payload versions axios@1.14.1 and axios@0.30.4 plus plain-crypto-js@4.2.1, 31 March 2026, https://www.theregister.com/security/2026/03/31/top-npm-package-backdoored-to-drop-dirty-rat-on-dev-machines/5219910); The Register, "Dev targeted by sophisticated job scam: 'I let my guard down, and ran the freaking code'" (Boris Vujičić / Genusix Labs, 23 April 2026, https://www.theregister.com/security/2026/04/23/dev-targeted-by-sophisticated-job-scam/5226263). The package.json prepare-script lifecycle hook and the npm install --ignore-scripts flag are documented at https://docs.npmjs.com/cli/v8/using-npm/scripts#life-cycle-scripts. The assembled URL https://rest-icon-handler.store/icons/77, the rest-icon-handler.store C2 domain, the 39 impersonated GitHub commits, the real-arts-journalist recruiter profile, and the agent-as-defensive-reviewer framing (Pi, read-only tools) are all Imankulov's. Conflict-of-interest note: the Imankulov post is a first-person incident write-up; he is the targeted candidate, the discoverer of the backdoor, and the author of the technical analysis. The framing of the backdoor as "the npm install was the backdoor" is editorial compression, not a direct quote. The HN comment from @pants2 on "LinkedIn offers no way for $company to disavow users who claim to work for $company" is summarized from the thread; the exact wording is on the HN page. The --ignore-scripts=true recommendation, the "pre-built sandbox image (Devbox, GitHub Codespaces, Daytona)" recommendation, and the "the threat model that catches this is at the recruiter layer, not the developer layer" framing are this blog's editorial position, not a direct prescription from Imankulov or The Register. Corrections from the first draft of this disclosure (applied 16 June 2026 morning): an earlier draft misattributed the 31 March 2026 axios compromise to maintainer Josh Junon via a 2FA-reset phish, and conflated the debug and chalk (~2B weekly downloads) compromise of 2025 with the axios payload. The Register's 31 March 2026 article attributes the axios compromise to maintainer jasonsaayman via an email-swap, with the listed payload versions above. Limit on inference: the C2 payload that rest-icon-handler.store would have served was not retrieved by Imankulov and was not retrieved for this post; the characterization "runs anything the server sends back to your machine" is Imankulov's read of the URL-construction code, paraphrased from his post. The current state of the malicious GitHub repo and the recruiter's LinkedIn account is taken from Imankulov's post; the post states the code is still up but does not state the recruiter account's current status, and no independent verification was attempted.

Sources

  • Roman Imankulov, "A backdoor in a LinkedIn job offer," 15 June 2026 — https://roman.pt/posts/linkedin-backdoor/ (canonical copy at https://web.archive.org/web/20260615230051/https://roman.pt/posts/linkedin-backdoor/)
  • Hacker News, item 48546294, "A backdoor in a LinkedIn job offer" — https://news.ycombinator.com/item?id=48546294 (point/comment counts moving; latest 568/109 in disclosure, fetched 16 June 2026 00:17 UTC)
  • The Register, "Top npm package backdoored to drop dirty RAT on dev machines," 31 March 2026 (axios / jasonsaayman / 2 versions: axios@1.14.1 + axios@0.30.4) — https://www.theregister.com/security/2026/03/31/top-npm-package-backdoored-to-drop-dirty-rat-on-dev-machines/5219910
  • The Register, "Dev targeted by sophisticated job scam: 'I let my guard down, and ran the freaking code,'" 23 April 2026 (Boris Vujičić / Genusix Labs / patch[.]sh) — https://www.theregister.com/security/2026/04/23/dev-targeted-by-sophisticated-job-scam/5226263
  • npm CLI documentation, "npm scripts — life cycle scripts" — https://docs.npmjs.com/cli/v8/using-npm/scripts#life-cycle-scripts
  • Snyk, "event-stream incident analysis" (background on the 2018 npm prepare-script pattern) — https://snyk.io/blog/event-stream-vulnerability/ (link was 404 as of 16 June 2026; referenced from the body, kept as a text mention for future correction when Snyk republishes the URL)
  • ESLint, "Postmortem for malicious package publishes" (2018 eslint-scope incident, same lifecycle-hook pattern) — https://eslint.org/blog/2018/07/26/postmortem-for-malicious-package-publishes (link was 404 as of 16 June 2026; keep as text mention, re-verify on the ESLint blog before next republish)

No comments:

Post a Comment