惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
Forbes - Security
Forbes - Security
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
Google DeepMind News
Google DeepMind News
MyScale Blog
MyScale Blog
GbyAI
GbyAI
B
Blog RSS Feed
S
SegmentFault 最新的问题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
小众软件
小众软件
The Last Watchdog
The Last Watchdog
W
WeLiveSecurity
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs
Hugging Face - Blog
Hugging Face - Blog
Microsoft Security Blog
Microsoft Security Blog
月光博客
月光博客
博客园 - 聂微东
F
Fortinet All Blogs
H
Hacker News: Front Page
A
About on SuperTechFans
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Check Point Blog
V
V2EX
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
N
News | PayPal Newsroom
Cisco Talos Blog
Cisco Talos Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Cloudflare Blog
P
Privacy & Cybersecurity Law Blog
N
Netflix TechBlog - Medium
C
CXSECURITY Database RSS Feed - CXSecurity.com
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
美团技术团队
Security Latest
Security Latest
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
Recent Announcements
Recent Announcements
The GitHub Blog
The GitHub Blog
Google Online Security Blog
Google Online Security Blog
T
The Exploit Database - CXSecurity.com

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
The Coding Challenge That Came for Your development Directory: Anatomy of a Job Interview Infostealer
Alex Chen · 2026-05-15 · via DEV Community

Last week I received a take-home assignment from a company calling itself a real estate technology firm. The email was well-formatted. The instructions were plausible. The package was a zipped Node.js project with a README that asked me to implement a data ingestion feature and return the result within forty-eight hours. Standard stuff. I've done thirty of these in the past year.

I did not run it.

What I found instead, after twenty minutes of static analysis, was a three-stage infostealer with a persistent socket backdoor, a browser credential exfiltration loop, and a recursive filesystem sweeper that specifically prioritizes ~/development, ~/Development, ~/Documents, and ~/Desktop — in that order — before crawling the rest of your home directory for anything matching the words "wallet," "private_key," "seed," "api_key," "token," ".env," or "password." The payload was 3.8 MB of obfuscated JavaScript. The legitimate-looking feature request was the delivery mechanism. The take-home was the pretext.

This post is the deobfuscation report, the anatomy lesson, and the checklist — written for the developer who has run twelve take-homes this year and never once audited what was in the package before executing npm install && npm start.

1. The shape of the attack

Before the technical details, the shape matters because the shape is what you need to recognize next time.

The attacker's goal is to get you to run node on their code. That's it. Everything else — the professional email, the Calendly invite for a follow-up call, the plausible-sounding feature description — exists to build enough trust that you type npm install && npm run dev without opening the package.json first. Once that command runs, the payload is executing. You will not see a terminal flash or a suspicious popup. The infostealer spawns three detached child processes and immediately returns control to the "real" application. The feature works. The tests pass. You submit your assignment. The data leaves in the background.

The variant I analyzed was embedded in a build script called from the project's postinstall hook — the hook npm runs automatically after npm install completes, before you've even read the README. This is not a novel technique. Malicious postinstall hooks have been a known attack surface since at least 2018. The novel part is the sophistication of what ran inside the hook and the deliberateness with which it targeted developers specifically: people who by definition have API keys, database credentials, private repositories, and crypto wallets, all sitting in a well-organized ~/development directory, all matching the payload's sensitive keyword list.

2. Three workers, three lock files, one temp directory

The payload's architecture is clean enough to admire in a grim way. On execution it spawns three detached Node.js processes by piping script strings to node - via child_process.spawn. Each worker is passed as a string of code written to the child's stdin — meaning the workers never exist as files on disk you might notice in a directory listing. Each worker writes a lock file to os.tmpdir() (pid.6.1.lock, pid.6.2.lock, pid.6.3.lock) to prevent duplicate instances. If a lock file already exists with a running PID, the payload kills the old process and spawns a fresh one. If the machine restarts and the lock files go stale, the next npm install on any project carrying this hook resurrects all three workers.

The three workers communicate with a command-and-control server at 144.172.117.220 on three different ports:

  • Port 8085 — browser credential and wallet file upload
  • Port 8086 — sensitive filesystem file upload
  • Port 8087 — real-time socket backdoor, beacon, and clipboard stream

When I captured the response headers, the server returned a Vercel edge response with a valid strict-transport-security header and a live x-vercel-id. The C2 was alive at the time of analysis. The upload endpoints were accepting connections.

3. Worker one: your browser is an unlocked credential store

The first worker targets Chromium-family browsers — Chrome, Brave, Edge, Opera, and their variants — across Windows, macOS, Linux, and WSL. It knows where each browser stores its profile data on each platform. It reads the Login Data SQLite database that every Chromium browser uses to store saved passwords, then it attempts to decrypt them.

The decryption flow is the part that surprises people who assume "saved passwords are encrypted." They are, but the encryption key is also stored locally, protected by the operating system's credential API. On macOS, the payload calls security find-generic-password -w -a 'Chrome' -s 'Chrome Safe Storage' to extract the Keychain key, then runs the PBKDF2 derivation that Chrome uses internally to produce the AES key, then decrypts the stored credentials. On Windows it uses PowerShell to call the DPAPI. On Linux it calls secret-tool. These are the same APIs your password manager's native app uses. The browser's designers never contemplated a script running in your terminal asking the OS for the key and the ciphertext in the same process.

The result is a plaintext dump of URLs, usernames, and passwords that gets uploaded to port 8085 every thirty seconds for up to ten cycles — five minutes of active exfiltration even if you close the terminal immediately after noticing something wrong.

Beyond passwords, the worker collects:

  • Session cookies from all profiles (allowing account takeover without the password)
  • Web Data (autofill, stored form values, potentially credit card numbers)
  • login.keychain-db on macOS
  • The extension storage directories for approximately fifteen browser crypto wallet extensions — MetaMask, Phantom, Exodus, Coinbase Wallet, Brave Wallet, Rabby, Coin98, and others

The wallet extension data is the prize for a certain class of attacker. Browser wallets store encrypted vault data locally. The encryption password is the user's wallet password. If that password is weak, the vault is offline-crackable. If the user has a hardware wallet passphrase set, the seed phrase is still in the extension's storage at rest. The payload does not attempt to crack the vault — it just uploads it and lets the C2 operator handle decryption offline, with GPU time they control.

4. Worker two knows where developers keep their secrets

The second worker is a recursive filesystem sweeper. It starts with a five-minute delay — enough time for the developer to submit the assignment and stop watching their terminal — then begins scanning.

The priority directory list is the thing I found most revealing about the attacker's intent:

~/Desktop
~/Documents
~/Downloads
~/Library/CloudStorage
~/Development
~/development
~/Code
~/code
~/Projects
~/projects
~/Source
~/source
~/OneDrive
~/Google Drive

Enter fullscreen mode Exit fullscreen mode

This is not a generic consumer infostealer. A generic consumer infostealer targets Documents and Downloads and stops there. This one explicitly lists ~/development, ~/Development, ~/Code, ~/Projects, ~/Source — the directories that exist on developer machines and essentially nowhere else. The attacker knows their target demographic. They know developers keep projects there. They know those projects have .env files. They know those .env files have the keys to your cloud infrastructure, your payment processor, your database, and your AI API accounts.

After the priority directories, the worker scans the full home directory with a keyword filter. Any file whose path contains one of these terms gets uploaded if it is under 5 MB:

  • Credentials: password, passwd, token, api_key, secret, credential, auth
  • Keys and certs: id_rsa, id_ed25519, .pem, .p12, .pfx, .jks, keystore
  • Crypto: wallet, seed, mnemonic, private_key, bitcoin, ethereum, metamask, phantom, ledger, trezor
  • Config files: .env, .yml, .yaml, .toml, .conf, .cfg, .ini, .properties
  • Documents: .pdf, .docx, .md, .txt, .csv, .xls, .xlsx
  • Databases: .db, .sqlite, .sqlite3, .sql
  • Source code: .js, .ts, .json, .jsx, .tsx, .xml
  • Images: .png, .jpg, .jpeg

That last category — images — is there for screenshots. Most developers have screenshots of dashboards, infrastructure diagrams, API key configuration pages, terminal sessions. Screenshots are a surprisingly rich data source for credential recovery.

The exclusion list is equally telling. The payload explicitly skips node_modules, .git, venv, dist, build, .next, .cache, vendor, and a hundred other paths that would generate noise without yielding secrets. This is tuned. Whoever wrote it understood the shape of a developer's filesystem well enough to write a precise filter. The inclusion and exclusion lists together constitute a detailed model of what a working developer's machine looks like.

5. Worker three: the clipboard is a live credential stream

The third worker maintains a persistent socket.io connection to the C2 server on port 8087. On connect, it immediately beacons your hostname, OS version, and username. It then does two things in parallel.

The first is an environment file hunt: after five minutes, it searches the entire filesystem for any file matching \.env* and uploads everything it finds to port 8085. Not just .env.env.local, .env.production, .env.staging, .env.test, .env.backup. Every variant. Every project.

The second is clipboard surveillance. The worker polls your clipboard approximately every second using platform-native tools — pbpaste on macOS, PowerShell's [System.Windows.Forms.Clipboard]::GetText() on Windows, xclip or xsel on Linux. Any new non-empty clipboard content is sent as a log event to /api/log on the C2 server.

Think about what passes through your clipboard on a working day. API keys copied from a dashboard to paste into a .env file. Database connection strings. SSH passwords. JWT tokens copied from a browser devtools console to test an endpoint. Wallet addresses. Seed phrases (if you are the kind of person who ever copies a seed phrase — you should not be, but people do). Every one of those clipboard snapshots, timestamped and tagged with your hostname and username, streams to the C2 in real time for as long as the worker is running.

The socket channel also accepts remote commands from the C2 operator. Command code 102 returns a directory listing. Command code 107 reads a specific file and exfiltrates it. Command code 108 bulk-uploads every file in a specified folder under 25 MB. This is a full remote shell in everything but name.

6. The evasion stack

The payload earned my grudging respect in one sense: the evasion design is layered, and each layer targets a different detection mechanism.

Obfuscation. The original file was 3.8 MB of obfuscated JavaScript — string tables, integer-key lookups, junk branch logic, anti-debug heartbeats that call setInterval every two seconds. Static analysis requires a dedicated deobfuscation pass before the behavior is readable. A developer who opens the file and sees 3.8 MB of compressed variable names closes it and moves on.

No files on disk. The workers are never written to disk as named files. They are strings piped to node - via stdin. ps aux shows a node process with no arguments except the interpreter. There is nothing in the filesystem to find.

Detached processes. spawn(..., { detached: true }) causes the workers to be reparented to PID 1 (or the Windows equivalent) on Unix systems. The parent process exits normally. The terminal returns. The workers keep running.

Swallowed exceptions. process.on("uncaughtException", () => {}) and process.on("unhandledRejection", () => {}) suppress all unhandled errors globally. The workers will not crash on malformed responses, revoked tokens, or unexpected filesystem layouts. They will silently continue.

Runtime dependency installation. If socket.io-client, axios, sql.js, or form-data are not installed in the victim's global npm environment, the payload installs them with npm install --no-save and re-requires them. It adapts to the victim's environment rather than assuming it.

Anti-debug noise. A setInterval every two seconds triggers a function that creates Function().constructor("return this")() — a construct that evaluates arbitrary code and makes tracing the execution graph noticeably harder. Most analysts give up before the behavior tree is fully mapped.

The combination of these techniques means that standard "run it in a VM and watch what happens" dynamic analysis still exposes the behavior — the network connections to port 8085/8086/8087 are clearly visible in a packet capture — but defeats casual inspection and makes it through most automated static analysis passes that look only for known bad strings.

7. What to do before you run any take-home assignment

I want to be precise here because "use a VM" is the advice everyone gives and almost nobody follows. VMs are slow to set up, inconvenient for multi-hour assignments, and give you a false sense of safety if you copy your real SSH keys or API credentials into them (which people do). Here is what actually fits into a realistic pre-run check that takes under ten minutes.

Read package.json first, completely. The scripts field is the attack surface. postinstall, preinstall, prepare — these run automatically. If any of these scripts call a file you have not read, read the file. If any of them call node scripts/something.js and scripts/something.js is 3.8 MB of minified code, that is your signal. Legitimate take-home assignments do not need 3.8 MB of compressed JavaScript in their build pipeline.

Check dependencies and devDependencies against the stated scope. An assignment that asks you to build a REST endpoint does not need socket.io-client, sql.js, or form-data unless those packages are relevant to the feature. Unexpected dependencies in a small project warrant investigation.

Grep the non-minified files for network calls. grep -r "http\|https\|socket\|fetch\|axios\|got\|request" --include="*.js" --include="*.ts" . on the unminified source. Legitimate take-homes do not phone home. If you see an IP address in a source file that is not localhost or a well-known API, stop.

Run npm install --ignore-scripts first. This flag tells npm to skip all lifecycle scripts (preinstall, postinstall, prepare, etc.) and just install the packages. Inspect everything, then decide whether to allow scripts.

Block outbound network for the duration. On macOS, pfctl can block outbound connections from a specific process. On Linux, firejail --net=none node . runs the process with no network access. If the application needs network access that is legitimate (calling the assignment's specified API), you will know about it from the README. Unexpected network connections during the interview task are not a feature.

None of this is foolproof. A sufficiently sophisticated payload will hide its indicators. But the category of attack I analyzed — interview-targeted Node.js infostealers — is not sophisticated in its delivery. It is sophisticated in its payload. The delivery relies on the developer's habit of trusting the interview context enough to skip the audit.

8. The upload infrastructure tells you something about the operator

The C2 infrastructure deserves a paragraph. The three upload endpoints are plain HTTP at 144.172.117.220, a cloud-hosted IP with no domain name. Port 8087 (the socket and beacon channel) is fronted through Vercel — the response headers include a x-vercel-id with an iad1 region marker, meaning the operator is proxying the WebSocket channel through Vercel's edge network for a different IP profile on port 443.

Each upload includes a validation HMAC computed with the secret SuperStr0ngSecret@)@^. Every file upload carries the victim's hostname, the file path (URL-encoded), a Unix timestamp, and the campaign marker userKey=609, t=6. This is a multi-tenant C2 — t=6 is the tenant or campaign identifier, and userKey=609 is likely a unique victim identifier assigned at delivery time. The operator can correlate all uploads from a single victim across all three workers using these markers.

The retry logic and validation scheme suggest a mature operation rather than a one-off experiment. A first-time attacker does not write exponential backoff logic with per-file retry counts and HMAC-validated uploads. Somebody has been running this for a while.

9. The incident response question you should answer first

If you are reading this after running an untrusted package, the first question is not "how do I remove it." The first question is "what was on this machine."

That question is harder and more important. If your ~/development directory was scanned, every .env file in every project was potentially uploaded. Every API key you have ever configured locally is now at risk. Every browser-saved password on this machine is potentially in the attacker's hands. Every piece of text you have copied to your clipboard in the time since the payload executed is logged.

The remediation checklist is:

  1. Rotate every API key and secret in every project, immediately, before doing anything else. Prioritize cloud credentials (AWS, GCP, Azure), payment processor keys (Stripe, Braintree), and AI API keys (Anthropic, OpenAI). A rotated key on a compromised project is useless to the attacker; an unrotated key is a live credential even after you've wiped the machine.
  2. Log out of every active session — GitHub, email, banking, cloud consoles, anything you access from this machine. Revoke OAuth tokens where possible. The session cookies from your browser profiles may have been exfiltrated.
  3. Revoke and regenerate SSH keys. Push the new public key to every service and host. Treat every existing id_rsa, id_ed25519, or .pem file as compromised.
  4. If you have any crypto wallet funds accessible from this machine, move them to a fresh wallet generated on an air-gapped device. Treat any seed phrase or private key that has ever been on this machine as known to the attacker, even if you stored it "securely."
  5. Check for running workers: ps aux | grep node. Look for pid.6.*.lock files in your temp directory ($TMPDIR on macOS, /tmp on Linux). Kill any lingering processes.
  6. Rotate any password or secret you may have typed or copied to clipboard during the infection window.

Step one takes longer than everything else. Do it first anyway.

10. The cultural problem that makes this attack work

There is a structural problem with the technical interview pipeline that this attack exploits, and naming it matters because fixing the checklist alone will not fix the underlying vulnerability.

Developers are conditioned to treat the interview context as a trust boundary. The hiring manager emailed you. They found you on LinkedIn. The assignment is from a real company (or appears to be). The psychological contract of "I am being evaluated so I should perform, not investigate" suppresses exactly the skepticism that would catch this attack. Nobody audits code they are about to be graded on the same way they would audit a random package. The social framing of the take-home is the exploit.

Attackers understand this. The real estate company framing in the sample I analyzed is plausible and unremarkable — real estate tech companies run take-home assignments constantly, the domain is boring enough not to raise questions, and the feature request (data ingestion pipeline) is exactly the kind of thing you would expect. Nothing in the email or the README flags as suspicious. The payload hides in a build script that runs before you read a single line of the application code.

The fix has to be a habit, not just a checklist. Make npm install --ignore-scripts your default for every unfamiliar project. Make reading package.json before running anything a reflex. Take the ten minutes to check network calls in the source before you type npm start. These are habits that cost you very little on legitimate assignments — legitimate assignments do not have malicious postinstall hooks — and that will catch the attack on the one in fifty assignments where the package is not what it claims to be.

The inconvenient truth is that the developers most at risk are the ones who are most active in the job market: recent grads doing thirty take-homes a year, experienced engineers exploring new roles, contractors evaluating client projects. These are also the people with the most API keys, the most cloud credentials, and the most active side projects sitting in a well-organized ~/development directory. The attack selects for exactly this profile, which is why the priority directory list says ~/development before it says ~/Documents.

You are the target because you are a developer. That knowledge should be inconvenient enough to make you open package.json before you type npm install.


The payload I analyzed has been reported to the hosting provider and to GitHub's security team. If you receive a take-home from a company you do not recognize with a Node.js project that includes a build script you cannot read, you are welcome to run the same grep queries I described in section seven. They take four minutes. They will not slow down your legitimate job search. They will, eventually, save you the experience of rotating fifty API keys at midnight while reading through what a three-stage infostealer took from your machine.