How a Fake LinkedIn Job Offer Delivered a Node Backdoor
Attackers are using stolen identities and malicious npm lifecycle scripts to target developers during the recruitment process.
The developer recruitment pipeline has increasingly become a high-value target for social engineering. Because software engineers are routinely asked to clone repositories, debug sample applications, and complete take-home assignments, they are uniquely primed to run untrusted code.
Recently, software developer Roman Imankulov exposed a highly targeted campaign where attackers used a fake LinkedIn job offer to deliver a backdoor payload. By combining identity theft with malicious npm lifecycle scripts, the attackers attempted to trick the developer into executing arbitrary remote code under the guise of a technical assessment.
The Bait: "Check the Deprecated Modules"
The attack began when an account posing as a recruiter for a small cryptocurrency startup contacted the developer on LinkedIn. After a brief exchange regarding a lead engineer position, the recruiter shared a public GitHub repository.
To make the request seem like a routine technical evaluation, the recruiter asked the developer to review a "broken proof-of-concept" and specifically instructed him to "check out the deprecated Node modules issue." This instruction was highly deliberate: resolving dependency issues naturally prompts a developer to run npm install to inspect the local dependency tree and verify warnings.
Suspicious of the interaction, the developer avoided cloning the repository locally. Instead, he provisioned a throwaway virtual private server (VPS) on Hetzner and analyzed the codebase using a read-only AI agent configured with strict file-reading limitations. The agent quickly flagged a suspicious file: app/test/index.js.
The Trigger: Exploiting npm Lifecycle Scripts
The repository was structured as a standard React frontend with a Node.js backend. The backdoor did not wait for a developer to run a test suite or start the application manually. Instead, it weaponized standard Node package management behavior.
In the project's package.json, the attackers configured the following scripts:
"scripts": {
"prepare": "npm run app:pre",
"app:pre": "node app/index.js"
}
The prepare script is a built-in npm lifecycle hook. By default, npm executes the prepare script automatically after local execution of npm install (without arguments).
When npm install was run, it triggered node app/index.js. Inside app/index.js, the code executed const test = require('./test'), which immediately loaded and executed the malicious payload hidden inside app/test/index.js.
Anatomy of the Payload
The backdoor was disguised within approximately 250 lines of code designed to look like a poorly written or commented-out test suite.
To evade basic static analysis and automated string matching, the code assembled its command-and-control (C2) URL from fragmented string variables:
const protocol = "https",
domain = "store",
separator = "://",
path = "/icons/",
token = "77",
subdomain = "rest-icon-handler",
bearrtoken = "logo";
When concatenated, these fragments resolved to https://rest-icon-handler.store/icons/77.
Buried deep within the commented-out test blocks, the active payload contained logic to make an HTTP request to this endpoint, retrieve a secondary payload, and execute it directly on the host machine. By serving the actual malicious payload dynamically from a remote server, the attackers kept the public GitHub repository clean of obvious malware signatures, reducing the likelihood of detection by automated repository scanners.
Double Identity Theft
What distinguished this campaign was the level of social engineering used to establish credibility. The attackers did not rely on freshly created, empty profiles. Instead, they hijacked the identities of two unrelated individuals:
- The Developer: The repository's commit history contained 39 commits, all authored under the name and email of a legitimate, active full-stack engineer. When contacted, the real developer confirmed he had no association with the startup, had never touched the repository, and had previously faced similar impersonation attempts.
- The Recruiter: The LinkedIn profile of the recruiter belonged to a well-known arts journalist with no technical background. However, when the target developer pretended he was having trouble installing the project, the account operator instantly shifted from non-technical recruiter speak to debating Node.js versions and insisting on running
npm install.
Defensive Takeaways for Developers
This attack highlights the vulnerability of relying solely on git commit signatures and LinkedIn profiles for trust verification. To defend against similar recruitment-themed attacks, developers should adopt several key practices:
- Isolate Untrusted Code: Never clone or run technical assessment code directly on a primary workstation. Use isolated virtual machines, throwaway VPS instances, or secure sandboxes.
- Disable Lifecycle Scripts: When inspecting unfamiliar repositories, run installation commands with the
--ignore-scriptsflag (e.g.,npm install --ignore-scripts). This preventsprepare,preinstall, andpostinstallscripts from executing automatically. - Audit package.json First: Before running any package manager commands, manually inspect the
package.jsonfile for unusual script definitions or unexpected binary dependencies.
Sources & further reading
- A backdoor in a LinkedIn job offer — roman.pt
Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.
Discussion 4
i'm so done with fake job offers, but using npm lifecycle scripts to deliver a backdoor is some next level stuff - it's 3am and i'm rewriting my entire recruitment process for no reason
i'm surprised they didn't also try to get the dev to install some 'required' tooling or library, that would've opened up even more avenues for exploitation, the fact that they're already using npm lifecycle scripts is bad enough
i'm not surprised, devs are always cloning and running random code, but using npm lifecycle scripts is a pretty clever trick - guess it's time to start paying closer attention to those package.json files 🚨
@lowlevel_lena yeah, those package.json files can be a treasure trove for attackers, i've seen some pretty nasty stuff hidden in postinstall scripts, time to add another item to our code review checklist