CSIPE

Published

- 35 min read

Fake Job Offers, Real Malware: How DPRK Operators Target Developers Through Interviews


The Digital Fortress: Your Everyday Guide to a Safer Digital Life

Stay Safe Online Without Making It Your Second Job

The Digital Fortress (Second Edition)

A warm, plain-English guide for people with real lives and finite patience. Learn the handful of habits that genuinely protect your money, accounts, and family, and get honest permission to ignore the rest.

Buy the book now
The Anonymity Playbook: Digital Survival for Whistleblowers, Journalists, Activists, and Everyone Else

For People Who Cannot Afford to Get Privacy Wrong

The Anonymity Playbook (Second Edition)

A practitioner’s field manual for journalists protecting sources, whistleblowers, and activists. It explains how the surveillance actually works, what each technique costs you, and exactly where it fails.

Buy the book now
Secure Software Development: Practical patterns for building secure software

Write, Ship, and Maintain Code Without Shipping Vulnerabilities

Secure Software Development

A hands-on security guide for developers and IT professionals who ship real software. Build, deploy, and maintain secure systems without slowing down or drowning in theory.

Buy the book now
The Secure Harness: Shipping Production Code with AI Coding Agents

Use AI Coding Agents Without Losing Control of Your Codebase

The Secure Harness

A calm, practical guide to letting agents do useful work inside boundaries you set, enforce, and audit. Ships with 15 copy-pasteable artifacts: hook scripts, permission configs, release gates, and MCP templates.

Buy the book now
The AI Native Engineer: Build, Evaluate, and Ship AI Systems That Work in Production

Stop Shipping Demos. Start Shipping Systems.

The AI Native Engineer

Sixteen hands-on chapters, one real product. Grow it from a single model call into a retrieved, tool-using, observable, production-grade system, with evaluation treated as a habit from the first feature.

Buy the book now

The pitch is the delivery mechanism

In July 2026 researchers documented DPRK-aligned operators posting fake job opportunities in developer Slack communities. Candidates who responded received a take-home coding challenge: clone this e-commerce repository, get it running, implement a feature.

The repository contained a Base64 payload concealed using SVG steganography, which unpacked into a four-stage malware chain aligned with the OTTERCOOKIE family. The stages covered credential theft, cryptocurrency wallet compromise, file exfiltration, and remote access. Infrastructure included the domain rightwidth[.]dev and the addresses 195.26.248[.]212 and 188.40.64[.]61. At the time of discovery, no antivirus engine detected it.

The campaign works because it inverts the usual advice. Developers are trained not to run untrusted code, and they are simultaneously expected to clone unfamiliar repositories constantly. A take-home exercise is a completely normal request that happens to require the exact behaviour an attacker needs.

The campaign behind the single incident

The July 2026 report describes one sighting. The operation behind it has been running for years, has a name, and has changed its packaging several times without changing its shape.

Researchers track it as Contagious Interview, and Microsoft’s March 2026 analysis places the earliest activity in December 2022. An operator poses as a recruiter for a cryptocurrency exchange, an AI startup, or a Web3 studio. The candidate receives an assessment. The assessment requires execution. Everything else is decoration.

What moves is the delivery surface. Early rounds relied on repositories cloned by hand from GitHub, GitLab, and Bitbucket. Later rounds pushed the payload into npm directly: Socket documented 338 malicious packages in a single wave with more than 50,000 cumulative downloads, following an earlier wave of 197 packages and roughly 31,000 downloads. The names are typosquats of things developers install without reading, epxresso for Express, dotevn for dotenv, ethrs.js and we3.js for the two libraries every crypto project pulls in. A candidate who never visits npm still installs them, because the trojanised assessment repository lists them as dependencies and npm install does the rest.

Newer variants skip the install step. Opening the cloned repository in Visual Studio Code raises the workspace trust prompt, and a .vscode/tasks.json configured to run on folder open fires the moment the developer clicks Trust. The only action taken was opening a folder in an editor.

The payloads come from a small, stable family:

  • BeaverTail is the original loader and stealer, present since the campaign began, now often used as a second-stage delivery mechanism rather than the first thing you meet.
  • OtterCookie appeared around September 2024 and grew from a simple command runner into modular theft with clipboard monitoring, virtual machine detection, and a socket.io channel held open to the operator.
  • InvisibleFerret is the Python remote access stage, deployed once a foothold exists.
  • FlexibleFerret, written in Go and Python, adds encrypted HTTP and TCP channels, dynamic plugin loading, and persistence through Windows RUN registry keys.

The loaders hide their second stage as hex strings decoded through eval, as XOR tables rebuilt by index arithmetic, or as AES-256-CBC ciphertext with the key and initialisation vector hardcoded a few lines away. Socket found samples storing that ciphertext inside the LICENSE file, which nobody reads and no scanner treats as code.

One detail from the Microsoft analysis is worth carrying forward: the recent samples are sloppy. Empty catch blocks, redundant reporting logic, tutorial-style comments, emoji in Python log output. The tradecraft is good and the code is mediocre, which points at volume rather than craft. Volume is what makes this likely to reach you.

npm install

open in editor

npm start

Recruiter contact via Slack, Discord or LinkedIn

Flattering role, above-market pay

Take-home: clone this repository

How does it execute?

preinstall / postinstall hook

.vscode/tasks.json on folderOpen

build script reads an asset

Loader: hex, XOR or AES blob

Stage two fetched at runtime, memory only

Credentials, keychain, wallets

Clipboard, screenshots, keylogging

Persistent RAT over HTTPS or socket.io

Cloud accounts, registries, private repositories

Why developers specifically

The targeting is deliberate and the reasoning is straightforward once you look at what a developer’s machine holds.

Source code for current and past employers, often including private repositories the developer still has access to. SSH keys and signing keys. Cloud credentials, frequently long-lived and frequently over-permissioned. Package registry tokens, which convert a workstation compromise into a supply chain attack against everyone who installs that package. Session cookies for internal tooling. And, given the overlap between software engineering and cryptocurrency, wallet files often enough to make wallet theft a standard stage.

Developers also routinely run unfamiliar code as part of the job, which normalises the risky behaviour. And they tend to have local administrator rights and security tooling exclusions on their machines, because endpoint agents interfere with compilers and containers, so somebody granted an exception years ago.

The financial motive is documented. Separate reporting from DTEX traced payment flows from DPRK IT workers embedded in hiring pipelines through team-collector accounts to Unit 1020 under Command 710, with references to a wallet identified as the OFAC-sanctioned Ryongbong General Corporation. Funds moved through Chinese financial channels and crypto-to-fiat conversion before consolidating to support weapons programmes.

Shrinking what a compromised workstation is worth

That inventory of what a developer machine holds is also the list of things you can move somewhere else. Sandboxing decides whether the payload runs; workstation hardening decides what the payload finds if it does, and the second question is the one you control every day rather than once per interview.

Start by taking an honest inventory. Most engineers are surprised by what is sitting in their home directory, because credentials accumulate through onboarding scripts, one-off debugging sessions, and tools that quietly cache a token on first use.

   # What a stealer would collect in its first two seconds.
ls -la ~/.ssh ~/.aws ~/.config/gcloud ~/.azure ~/.kube 2>/dev/null
grep -l "_authToken" ~/.npmrc ~/.yarnrc.yml 2>/dev/null
cat ~/.docker/config.json 2>/dev/null | head -20
git config --global --get credential.helper
env | grep -Ei 'token|secret|key|password' | cut -d= -f1
find ~ -maxdepth 4 -name '.env' -not -path '*/node_modules/*' 2>/dev/null | head -20

Every line that returns something is a line in the incident report you would otherwise be writing. Work through them by category, and prefer changes that make the artefact useless when stolen over changes that try to hide it better.

What you holdHow it usually livesMove it toWhat the payload gets instead
Cloud accessLong-lived access keys in ~/.aws/credentialsSSO or OIDC with one-hour sessionsAn expired session token
SSH accessA private key file on diskA key in the Secure Enclave or on a hardware token, touch required per useA public key and a prompt nobody presses
Commit signingA GPG key with a cached passphraseHardware-backed signing, touch requiredNothing signable
Registry publishingA personal _authToken in ~/.npmrcTrusted publishing from CI, no local tokenNo publish path
Application secrets.env files scattered across projectsA secrets manager fetched at run timeEmpty files
Internal toolingLong-lived session cookiesShort session lifetimes with device-bound checksCookies that fail at the next request

The hardware-backed key row carries more weight than the rest combined. A stealer copies files, and a key that cannot leave a security chip is not a file. Adding a physical touch requirement to every signature and every SSH authentication also gives you a live detector: a touch prompt appearing while you are reading a pull request tells you something is using your key without asking, which is a signal no log would have given you that fast.

Egress visibility is the second control worth the effort. An outbound filter on the workstation, Little Snitch or LuLu on macOS and opensnitch on Linux, turns a silent exfiltration into a dialog box. The first week is noisy while you approve the things you actually use, and after that the alerts are meaningful. Microsoft’s guidance for this campaign makes the same point from the network side: watch for outbound requests to newly registered domains and link shorteners, and for the classic download-and-execute shape of a shell fetching a script and piping it straight into an interpreter.

Then take the exclusions seriously. Almost every engineering organisation has a folder-level antivirus exclusion granted years ago because the endpoint agent was fighting the compiler, and almost nobody has revisited it. Those exclusions are frequently ~/Projects or ~/src, which is precisely where a cloned assessment lands. Narrowing an exclusion to a specific build output directory keeps the performance win and removes the free pass.

Two smaller habits round it out. Keep a separate browser profile for work identity providers so a stolen cookie jar from casual browsing carries nothing useful. And treat your password manager as unlocked-only-when-needed rather than unlocked-all-day, because these families specifically enumerate vault artefacts from KeePass and 1Password, and a locked vault at the moment of execution is the difference between losing one credential and losing all of them.

None of this depends on recognising the lure. The recruiter script will change, the repositories will get better history, and the payload will move to whatever asset format nobody is scanning. A workstation where the most valuable credential is a one-hour session token survives all of those changes without needing an update.

Reading the lure

The recruitment side has recognisable characteristics, and knowing them costs nothing.

Contact comes through a channel with no verification, which increasingly means a Slack or Discord community rather than a job board. The role is described in flattering, slightly generic terms with compensation above market. The process moves fast and skips steps you would expect, and the recruiter is reluctant to move to a video call or a corporate email domain.

The technical exercise is the tell. Legitimate take-home tests are small, self-contained, and reviewed as source code. They rarely require you to run a full application stack, they do not require you to install unusual dependencies, and they do not need you to execute a build before reading the code. A challenge that only proves itself by running is an odd design for an assessment and a sensible design for a delivery mechanism.

Watch for urgency around execution specifically. “Run npm install && npm start and tell me what error you see” is a request to execute arbitrary code, phrased as debugging.

Telling a real take-home from a delivery mechanism

The recruitment signals are soft, and a well-run scam gets most of them right. The exercise itself is harder to fake, because a real assessment and a delivery mechanism are built for different purposes, and the difference shows up in the artefacts.

SignalLegitimate take-homeDelivery mechanism
Repository historyDozens of commits, several authors, months of activityOne commit importing thousands of files, one author, created last week
ScopeA single problem across a handful of filesA full application stack with a database and a build pipeline
What you submitSource code, a diff, a pull requestA screenshot or a recording proving that it ran
Setup instructionsnpm ci && npm testEight steps, one of which weakens a security control
DependenciesFew, familiar, pinned in a lockfile you can readUnusual names, unpinned ranges, or a registry override in .npmrc
ExecutionOptional; the tests can run in CIMandatory before you can understand the brief
Time budgetStated up front, usually two to four hoursVague, with pressure to reply tonight
ReviewA human reads your code and argues about itThe recruiter mainly wants to know that it started
ChannelApplicant tracking system, corporate email domainDirect message, Telegram, personal Gmail, shortened link
AssetsSmall and boring images and fixturesAn SVG, LICENSE or config file carrying a long encoded run

Two of those rows outrank the rest. The first is any instruction that puts running before reading, because a company assessing your engineering judgement wants your code, and only an attacker needs your process. The second is any instruction to weaken a control so the project will build. Adding an antivirus exclusion for the project folder, disabling Gatekeeper on macOS, running the terminal as administrator, or pinning to an end-of-life Node version because “the newer ones break the build” all describe the same request in different words.

The single-commit repository deserves its own note. A genuine take-home is usually a small template maintained over time by the hiring team, so its history reads like a project. A trojanised one is assembled, squashed, and pushed in one go, because the operator generated it from a stolen or scaffolded codebase and dropped the payload in before publishing. Checking git log takes four seconds and answers a question that no amount of reading the README will.

There is a variant that skips the pretence of an assessment altogether. The recruiter says the repository has a bug, sends you the clone URL, and asks you to reproduce the error and describe what you see. Reproducing the error is the entire attack. Nothing about the exchange requires you to write a line of code, which is what makes the request feel low-effort and safe.

Where the payload hides

The technical craft here is worth understanding because it defeats casual inspection.

Steganography in an SVG works because SVG is XML, and XML tolerates a great deal of content that renderers ignore. A Base64 blob can live in a comment, an unused attribute, a <metadata> element, or a <desc> block. The image displays normally. A reviewer skimming the repository sees an icon.

The loader is then a small piece of ordinary-looking code, often inside a build script or a postinstall hook, that reads the asset, extracts the payload, and executes it. Neither half is suspicious alone. The SVG is an image and the build script is a build script.

Multi-stage delivery defeats signature scanning by ensuring the malicious code is never on disk in a recognisable form until execution. Stage one is a tiny downloader, and the substantive payload arrives from the network at runtime and may live only in memory. That is why the antivirus detection rate was zero: there was nothing on disk that resembled known malware.

   # Before running anything, look at what executes on install.
# npm: these fields run automatically on `npm install`.
jq '.scripts | {preinstall, install, postinstall, prepare}' package.json

# Look for encoded blobs in assets, which is where the payload hides.
grep -rIl --include='*.svg' --include='*.json' --include='*.md' \
  -E '[A-Za-z0-9+/]{200,}={0,2}' . | head

# What does the build actually fetch?
grep -rn --include='*.js' --include='*.ts' --include='*.json' \
  -E 'curl|wget|https?://[^"'"'"' ]+' package.json scripts/ 2>/dev/null | head -20

Inspecting an untrusted repository before you run it

Those three commands are the start of a routine rather than the whole of it. Cloning a repository copies bytes and executes nothing, which buys you an inspection window of about ten minutes, and the decision gets made inside that window. Do the inspection from a terminal or a plain text editor. Opening the folder in a full IDE is already a form of running it.

Start with provenance, because it is cheap and it is often decisive.

   git clone --depth 50 --no-tags https://example.invalid/candidate-app repo
cd repo

# A real project has history. An import has one commit and 3,000 files.
git log --oneline | wc -l
git log --format='%an <%ae>' | sort -u
git show --stat --oneline HEAD | tail -5

# Anything that runs when an editor or dev container opens the folder.
cat .vscode/tasks.json .vscode/settings.json 2>/dev/null
cat .devcontainer/devcontainer.json 2>/dev/null
ls -la .husky .githooks 2>/dev/null

Then look at what wants to execute during dependency installation. The root package.json is the file everyone checks, and the interesting hooks are usually four directories down inside a dependency nobody has heard of. Fetch the tree without running anything, then enumerate every lifecycle script in it.

   # Root scripts first.
npm pkg get scripts

# Resolve the whole tree with hooks disabled.
npm install --ignore-scripts --no-audit --no-fund

# Now list every dependency that wanted to run something on install.
node -e '
const fs = require("fs"), path = require("path");
const hooks = ["preinstall", "install", "postinstall", "prepare", "prepublish"];
(function walk(dir) {
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const p = path.join(dir, entry.name);
    if (entry.isDirectory()) { walk(p); continue; }
    if (entry.name !== "package.json") continue;
    let pkg;
    try { pkg = JSON.parse(fs.readFileSync(p, "utf8")); } catch { continue; }
    const scripts = pkg.scripts || {};
    const found = hooks.filter((h) => scripts[h]);
    if (found.length) {
      console.log(pkg.name, pkg.version, found.map((h) => `${h}: ${scripts[h]}`).join(" | "));
    }
  }
})("node_modules");
'

A handful of results is normal. Native modules build on install, and esbuild and friends fetch platform binaries. What you are reading for is a hook that curls a URL, writes to a temporary path and executes it, or reads a file from the repository’s own asset directory.

Encoded payloads are the next pass. The grep in the previous section finds long Base64 runs; this version also catches hex, decodes what it finds, and prints anything that looks like text so you can judge it without running it.

   #!/usr/bin/env python3
"""Find long encoded runs in a repository and show what they decode to."""
import base64
import binascii
import pathlib
import re
import sys

B64 = re.compile(rb"[A-Za-z0-9+/]{240,}={0,2}")
HEX = re.compile(rb"(?:[0-9a-fA-F]{2}){120,}")
SKIP = {".git", "node_modules", "dist", "build", ".next"}


def scan(root: pathlib.Path) -> None:
    for path in root.rglob("*"):
        if not path.is_file() or SKIP & set(path.parts):
            continue
        try:
            blob = path.read_bytes()
        except OSError:
            continue
        if len(blob) > 8_000_000:
            continue
        for pattern, decode in ((B64, base64.b64decode), (HEX, binascii.unhexlify)):
            for match in pattern.finditer(blob):
                try:
                    out = decode(match.group(0))
                except Exception:
                    continue
                head = out[:120]
                printable = sum(32 <= c < 127 for c in head) / max(len(head), 1)
                if printable > 0.80:
                    print(f"{path}: {len(match.group(0))} chars -> {head!r}")


if __name__ == "__main__":
    scan(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "."))

Run it against the LICENSE file too. Socket’s analysis found AES ciphertext parked there specifically because reviewers skip legal boilerplate.

The last pass is for execution primitives and for anything that redirects where code comes from.

   grep -rn --exclude-dir=node_modules --exclude-dir=.git \
  -E "eval\(|new Function\(|child_process|execSync|spawnSync|vm\.run|atob\(|Buffer\.from\([^)]*base64" .

# Where do the packages actually come from?
cat .npmrc 2>/dev/null
grep -n '"resolved"' package-lock.json | grep -v 'registry.npmjs.org' | head

A single hit proves nothing. Build tooling uses child_process for legitimate reasons all day long. The finding you are hunting is a combination inside one call chain: an asset gets read, something decodes it, and the result reaches an interpreter. Any file that does all three in twenty lines has told you what it is.

Deciding whether to run it

Inspection produces evidence; the decision still has to be made, usually while a recruiter is waiting for a reply. Writing the rule down in advance removes the part where you argue with yourself at 11pm.

No

Yes

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Repository cloned, nothing run yet

Sender verified through
a corporate channel?

Disposable environment only

Install hooks or
editor-triggered tasks?

Encoded blobs in assets,
LICENSE or config?

Do not run. Report it.

Brief requires disabling
a security control?

Unpinned deps or a
non-default registry?

Host holds keys, tokens
or wallets?

Run it, with egress logged

Follow the branches honestly and the tree collapses into one rule for almost everybody: unfamiliar code runs in a disposable environment, always, because the final question is answered “yes” on every working developer’s laptop. The tree earns its keep at the two nodes marked stop, which are the cases where the correct move is to walk away and hand the repository to your security team rather than to evaluate it more carefully.

The reporting branch matters more than it looks. A campaign that reaches one engineer in a Slack community has reached everyone in that community. Posting the recruiter handle and the repository URL in your internal security channel takes a minute and turns your near miss into other people’s early warning.

Two of the branches deserve a caveat, because the tree gives a clean answer to a messy input. A verified corporate channel means an email from a domain you resolved yourself and a person you found on the company’s own site, rather than a Slack handle with a company logo as an avatar. And “unpinned dependencies” covers more than a caret in package.json: a lockfile can resolve to a mirror, a Git URL, or a tarball on someone’s object storage, which is why the lockfile grep earlier is part of the same question rather than a separate one.

The employment fraud running alongside it

The interview lure is one half of a larger operation, and seeing both halves explains why the campaigns are so persistent.

The other half is employment itself. DPRK IT workers apply for remote engineering roles under fabricated identities, get hired by companies who believe they are contracting an engineer in a permitted jurisdiction, and do the work. They are frequently competent, which is why the arrangement survives review. The salary is the objective.

DTEX’s July 2026 reporting traced the money. Workers confirm completed payments through a coordination site, luckyguys[.]site, and funds route through team-collector accounts to Unit 1020 under Command 710. References to an “RB wallet” in the operators’ own communications map to Ryongbong General Corporation, an entity under OFAC sanctions. From there money moves through Chinese financial channels and crypto-to-fiat conversion before consolidating to support weapons programmes and military operations.

For a company, the exposure is broader than a compromised laptop. Paying a sanctioned entity, even unknowingly through a contractor arrangement, is a sanctions violation with its own legal consequences separate from any security incident. And an engineer with legitimate commit access for eighteen months is a substantially deeper access problem than a single malware infection.

The screening signals that surface this are mostly logistical rather than technical. Reluctance to appear on camera, or appearing with conditions that make identification hard. A work location that disagrees with network telemetry. Payment routing that changes to a different country or to a cryptocurrency address. Multiple candidates sharing a phone number, a document template, or a portfolio site. Requests to ship equipment to an address other than the stated residence, which is the pattern behind the laptop farm services that support these placements.

Hiring-side controls for engineering managers

Those screening signals describe what to look for. Turning them into controls means deciding who checks what, at which stage, with what authority to stop the process. A signal that nobody owns produces a conversation after the incident and nothing before it.

The scale argues for spending the effort. Reporting across the tracked campaigns puts the number of job applications submitted under fabricated identities above 170,000, and estimates the scheme’s 2024 revenue at close to 800 million dollars. The 2026 Verizon Data Breach Investigations Report gave the pattern its own section. Nisos makes the point that standard background checks answer the wrong question: they confirm that the identity presented is real, and say nothing about whether the person in the interview is attached to it.

StageControlWhat it catches
ApplicationCross-source identity checks covering account age, breach history, reused portfolio templatesFresh identities with no digital past, VoIP numbers, resumes mirroring the job ad
Screening callAn unscheduled camera-on segment, no virtual background permittedCamera avoidance, a proxy sitting the interview
Technical roundLive coding on a shared editor plus follow-up questions about the submitted take-homeWork produced by someone other than the candidate
OfferGovernment ID with a liveness check through a verification vendorStolen or synthetic identity documents
EquipmentShipping only to the verified residential address, with an unboxing photo showing the ID holderLaptop farms and forwarding addresses
PayrollA freeze on payment routing changes for the first 90 days, human re-verification for any change afterRerouting to team-collector accounts
First 90 daysScoped access, no production credentials, endpoint review for remote control softwareMid-contract handover to a different operator
OngoingImpossible-travel and residential-proxy alerting on the identity providerWork location that disagrees with the contract

Two of those do most of the work. Identity verification with a liveness check at offer stage defeats the document-only fraud that carries most of these placements, and it costs a few pounds per hire. Controlling equipment delivery defeats the laptop farm, which is the load-bearing piece of infrastructure in the whole scheme; a worker in Pyongyang needs a machine sitting in a permitted country with someone to plug it in.

Apply all of it uniformly. Targeting the checks at candidates with foreign addresses or unfamiliar accents produces discrimination and worse detection at the same time, because the operators pick identities that look domestic precisely to defeat that instinct. A control applied to everybody is also a control nobody can argue with.

Engineering managers have one piece of authority that HR does not, which is the ability to slow a requisition down. The pressure that opens the door is a headcount that had to be filled last quarter and a candidate who is available immediately, competent in the interview, and cheaper than the market. Every one of those attributes is exactly what the operation optimises for. Treating a candidate who is unusually available, unusually cheap, and unusually eager to skip steps as a reason for more scrutiny rather than less is a habit that costs nothing and closes the most common entry.

Finally, give people a route to raise a suspicion about a colleague without accusing anybody in public. Most of these placements are eventually noticed by a teammate who finds the working hours strange or the video call always broken, and that observation reaches nobody if the only available action is a public accusation.

If you already ran the repository

Assume compromise and work quickly, because the credential theft stage runs within seconds of execution.

Disconnect the machine from the network first, which stops exfiltration in progress and prevents the remote access stage from receiving instructions. Do not reboot: memory contents are useful evidence, and several stages exist only in memory.

Then rotate credentials from a different, known-clean machine, in order of blast radius. Cloud provider credentials and any long-lived access keys come first. Package registry tokens next, because those enable supply chain attacks against your users and the window matters. SSH keys and signing keys after that, remembering to remove the old public keys from every service rather than only adding new ones. Session cookies for internal tooling need invalidating server-side, since changing a password does not always terminate existing sessions. Cryptocurrency wallets, if any were on the machine, should be treated as drained and any remaining balance moved immediately.

Tell your security team before you finish rotating, not after. The organisational response, checking whether the credentials were used, reviewing repository access logs, and looking for the same infrastructure elsewhere in the estate, depends on knowing quickly. The instinct to quietly clean up first and report once it is under control is understandable and costs the response its most useful hours.

Rebuild the machine rather than cleaning it. The remote access stage is designed to persist, detection was zero at time of discovery, and the cost of a rebuild is a day against the cost of retaining a foothold.

The first hour, in order

Those steps are the right ones, and sequencing them matters as much as performing them, because two pairs of them pull against each other. Preserving memory conflicts with stopping exfiltration. Rotating credentials conflicts with reporting, if you treat the two as sequential.

Resolve the first by pulling the network cable or switching off the wireless radio while leaving the machine powered on. Resolve the second by refusing to treat them as sequential: you report from the clean machine, then keep rotating while somebody else starts pulling logs.

Platform and IdPSecurity teamClean deviceDeveloperPlatform and IdPSecurity teamClean deviceDeveloperDisable Wi-Fi, unplug ethernet, leave it powered onMove to a second, known-clean machineReport repo URL, recruiter handle, exact commands, timestampRevoke cloud keys and active assumed-role sessionsRevoke npm, PyPI and container registry tokensInvalidate SSO sessions server-sideRemove old SSH and signing public keys everywherePull audit logs from the execution timestamp onwardSweep the estate for the same domains and addressesCapture volatile evidence, then rebuild the host

Order the revocations by what an attacker converts to value fastest rather than by what feels most alarming. Cryptocurrency wallets are drained within minutes and there is nothing to salvage after that. Package registry tokens come next, because a publish from your account turns one laptop into every downstream install, and the response window is measured in hours. Cloud keys follow, then version control tokens, then session cookies. Remember that removing an SSH key means deleting the old public key from GitHub, GitLab, your bastion hosts, and every authorized_keys file it was ever copied into.

The single most valuable thing you hand the security team is the timestamp. Every log query they run is anchored to it, and “sometime yesterday afternoon” turns a twenty-minute investigation into a two-day one. Write down the clock time you ran the command, the exact command, whether your password manager was unlocked at the time, and whether an SSH agent was running with keys loaded. Those four facts define the blast radius.

If your organisation has anyone who can capture a memory image, they need the machine powered on and off the network. If nobody can, say so early rather than leaving a compromised laptop running for three days waiting for a capability that does not exist. Photograph the screen, note the running processes if you can do it without touching the network, and move to rebuilding.

Running strangers’ code safely

The habit that closes this attack is separating reading from running, and treating the second as a decision.

Clone and read first, on your normal machine, without installing. Cloning a repository does not execute anything. Reading it costs ten minutes and catches the obvious cases: an install hook you did not expect, an asset with a suspicious payload, a dependency you have never heard of pinned to an unusual registry.

Then run it somewhere disposable. A virtual machine you can delete, a cloud development environment, or a container with no access to your home directory. The specific technology matters less than the property that a compromise loses nothing. npm install with --ignore-scripts is a useful additional layer for inspection, though it will not produce a working application for anything that legitimately needs a build step.

   # Read-only inspection, no execution, on a throwaway container.
docker run --rm -it \
  --network none \
  -v "$PWD/candidate-repo:/repo:ro" \
  node:22-slim bash

Keep credentials off the machine that runs unfamiliar code. This is the control that determines what an incident costs. If the environment where you evaluate a take-home has no SSH keys, no cloud credentials, and no package registry tokens, then the worst case is a destroyed container.

Building an evaluation environment you can throw away

That container one-liner covers reading. Running a full application stack needs something with a package manager, a working network, and no path back to anything you care about. Three setups cover almost every case, and the right one depends on how much of the stack has to come up.

Start with a dev container that you own. The important word is own: a repository’s own .devcontainer/devcontainer.json is written by the person you do not trust, and its postCreateCommand runs on your behalf the moment you click Reopen in Container. Keep your own template outside the repository and point it at the code.

   {
	"name": "untrusted-eval",
	"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
	"runArgs": [
		"--cap-drop=ALL",
		"--security-opt=no-new-privileges",
		"--pids-limit=512",
		"--memory=4g",
		"--network=eval-net"
	],
	"containerEnv": { "NPM_CONFIG_IGNORE_SCRIPTS": "true" },
	"mounts": [],
	"remoteUser": "node",
	"overrideCommand": true,
	"postCreateCommand": "",
	"customizations": {
		"vscode": { "extensions": [] }
	}
}

Note the empty extensions array and the empty postCreateCommand. Extensions activate on file open and can execute project-local binaries out of node_modules/.bin, so an evaluation container wants none of them. Leave workspace trust enabled and open the folder in Restricted Mode for the reading pass.

For anything that needs a database and a running server, Compose gives you the property that matters most: an application network with no route off the host except through a proxy that writes down every request.

   name: untrusted-eval
services:
  app:
    image: node:22-bookworm-slim
    working_dir: /work
    command: sleep infinity
    user: '1000:1000'
    cap_drop: ['ALL']
    security_opt: ['no-new-privileges:true']
    tmpfs: ['/tmp:size=512m']
    mem_limit: 4g
    pids_limit: 512
    networks: [evalnet]
    volumes:
      - ./candidate-repo:/work
    environment:
      HTTP_PROXY: 'http://proxy:3128'
      HTTPS_PROXY: 'http://proxy:3128'
  proxy:
    image: ubuntu/squid:latest
    networks: [evalnet, egress]
    volumes:
      - ./squid.conf:/etc/squid/squid.conf:ro
networks:
  evalnet:
    internal: true
  egress: {}

internal: true removes the default gateway from the application network, so the only way out is the proxy, and the proxy access log becomes the artefact that answers the question you actually care about: did this assessment contact anything other than the registry? Point squid.conf at an allowlist containing registry.npmjs.org and whatever the README claims it needs, and deny the rest.

When the exercise wants Docker itself, or when the code is native rather than interpreted, move up to a virtual machine. Multipass is the shortest path on macOS and Linux.

   multipass launch 24.04 --name eval --cpus 4 --memory 8G --disk 40G
multipass exec eval -- bash -lc 'sudo apt-get update && sudo apt-get install -y nodejs npm'

# Copy the code in. Do not mount a host directory into the VM.
multipass transfer --recursive ./candidate-repo eval:/home/ubuntu/repo
multipass exec eval -- bash -lc 'cd repo && npm install --ignore-scripts && npm test'

# When you are done, destroy it rather than stopping it.
multipass delete eval && multipass purge

Cloud development environments look attractive for this and carry a specific trap. A hosted workspace usually arrives with a token minted for your account, and on the common defaults that token reaches your private repositories. If you use one, create a throwaway account with no organisation membership, and check what is sitting in the environment before you run anything.

The rule that makes all four options work is the same: you never authenticate anything inside the evaluation environment. No gh auth login, no aws configure, no pasting a token to make the build finish. The moment a credential enters the sandbox, the sandbox stops being disposable.

What each sandbox actually protects

Isolation options get described as though the differences between them were small. They are not equivalent, and being specific about what each one contains lets you pick deliberately rather than by habit.

OptionBlocks host credential theftBlocks LAN accessSurvives a kernel exploitSetupVerdict
Container with default flagsPartialNoNo1 minFine for reading, weak for running
Container, --network none, no mounts, caps droppedYesYesNo2 minGood for static inspection
Compose with an internal network and logging proxyYesYesNo15 minBest day-to-day choice
Local VM (Multipass, UTM, VirtualBox)YesOnly with host-only networkingUsually20 minRight for full stacks and native code
Disposable cloud VMYesYes, in its own VPCYes, from your estate10 minGood, watch the attached instance role
Cloud dev environmentDepends on injected tokensNoYes, from your laptop2 minOnly with a clean throwaway account
Separate physical machineYesOnly on a guest networkYesHoursWorth it if you evaluate often
A second local user accountNoNoNo5 minNot isolation in any useful sense

Three configuration choices void every row in that table. Mounting your home directory hands over the exact files the payload came for. Mounting the Docker socket lets any process inside start a privileged container with the host filesystem attached, which is a root shell with extra steps. Forwarding the SSH agent lets the sandbox sign operations with keys it cannot read, which is enough to push to your repositories.

Be honest about the kernel row. Containers share the host kernel, so a container contains ordinary malware and accidents, and does not contain a determined kernel escape. For this particular threat that limitation rarely bites, because the payload is JavaScript hunting for files, tokens and wallets rather than an exploit chain. Containers are usually enough here. Say that plainly, and reach for a VM when the code is native, when it wants privileged access, or when the stakes make the extra fifteen minutes cheap.

The cloud VM row hides the most common real-world failure. An instance launched in your own account inherits an instance role, and instance roles are frequently broader than anybody remembers. Launch evaluation instances in a separate account or project with no attached role at all.

Testing that the sandbox actually holds

An isolation setup you have never tested is a belief rather than a control. Testing one takes about five minutes, you do it once per template, and it converts an assumption into something you can point at. Write a probe that behaves the way the payload would and confirm that every attempt fails.

   #!/usr/bin/env bash
# sandbox-selftest.sh - run this INSIDE the evaluation environment.
set -u
fail=0
check() {
  if eval "$2" >/dev/null 2>&1; then
    echo "FAIL: $1"; fail=1
  else
    echo "ok:   $1"
  fi
}

check "no host SSH keys visible"     '[ -s "$HOME/.ssh/id_ed25519" ] || [ -s "$HOME/.ssh/id_rsa" ]'
check "no cloud credentials visible" '[ -s "$HOME/.aws/credentials" ] || [ -n "${AWS_ACCESS_KEY_ID:-}" ]'
check "no registry token visible"    'grep -q "_authToken" "$HOME/.npmrc"'
check "no git credential helper"     'git config --get credential.helper'
check "no docker socket"             '[ -S /var/run/docker.sock ]'
check "no forwarded SSH agent"       '[ -n "${SSH_AUTH_SOCK:-}" ]'
check "no host LAN reachable"        'curl -s --max-time 3 http://192.168.1.1 -o /dev/null'
check "no cloud metadata reachable"  'curl -s --max-time 3 http://169.254.169.254/ -o /dev/null'
check "no direct internet egress"    'curl -s --max-time 5 --noproxy "*" https://example.com -o /dev/null'

exit "$fail"

The metadata check is the one that catches cloud sandboxes built carelessly, since 169.254.169.254 is how a payload turns an instance into whatever the instance role can reach. The egress check uses --noproxy deliberately: in the Compose setup, traffic through the proxy should succeed and traffic around it should fail, and testing both tells you the difference.

Then verify the proxy log after a real evaluation run, because the log is the point of the whole arrangement.

   docker compose exec proxy tail -n 200 /var/log/squid/access.log
# Anything that is not the package registry or a documented API is a finding.

Plant a canary while you are at it. A fake AWS access key from a canary token service, dropped in the sandbox home directory, costs nothing and turns silence into evidence: if the key is ever used, an alert arrives with the attacker’s source address attached. The same trick works with a decoy wallet.dat or a fake .env.

Last, verify that destruction actually destroyed something. This is the step people skip, and a “temporary” environment still running three weeks later with a remote access stage inside is a worse outcome than never sandboxing at all.

   docker compose down -v --remove-orphans
docker volume ls | grep untrusted-eval   # expect no output
multipass list                           # expect no eval instance

Common mistakes

Every failure below has shown up in a real incident, and each one involves somebody who believed they had taken the safe route. Reading them is faster than earning them.

  • Sandboxing the run but not the read. Opening the folder in your editor on the host is a decision to execute. The trust prompt is the last checkpoint, and formatters and linters activate on file open and happily execute project-local binaries.
  • Mounting your home directory. -v $HOME:/home/node delivers the SSH keys, the cloud credentials and the shell history straight into the container.
  • Mounting the Docker socket. Any process inside can launch a privileged container with the host root filesystem attached.
  • Forwarding the SSH agent. The sandbox cannot read the key and can still sign with it, which is all a push to your repositories requires.
  • Using the repository’s own dev container. You are trusting a configuration file written by the person under evaluation.
  • --ignore-scripts followed by npm run build. Skipping the lifecycle hooks and then running the build by hand executes the same code with extra ceremony.
  • Judging trust by stars and forks. Both are purchasable, and both come along for free when an operator forks a real project and adds one commit.
  • Treating a clean scan as a result. The July 2026 sample had zero antivirus detections at the time of discovery.
  • Assuming macOS or Linux is out of scope. These families ship per-platform stages, and keychain and wallet theft on macOS is a first-class objective rather than an afterthought.
  • Snapshot-restore instead of destroy. A restored virtual machine keeps everything that persisted outside the snapshot boundary, including mounted directories and any credential attached to the instance.
  • Cleaning up before reporting. The audit log window is at its most useful in the first hour, and quiet remediation spends it.

Behind most of that sits one meta-mistake worth naming: optimising for the candidate experience. Engineers run these repositories because refusing feels rude, because the sandbox takes twenty minutes they have not budgeted, and because a recruiter is waiting. The fix is social rather than technical. Say out loud, in your engineering channel, that “I will read the code today and run it in a sandbox tomorrow” is an acceptable reply to any take-home, and that a hiring process which cannot tolerate that answer has told you something useful about itself.

The same permission applies to the person on the other side of the table. If you send take-home exercises, make them runnable from source with a lockfile and no install hooks, say so in the README, and accept a diff instead of a screenshot. Designing an assessment that a careful candidate can complete safely costs an hour once and removes your company from the set of employers whose process is indistinguishable from an attack.

For teams

Two organisational measures address this beyond individual habits.

Tell people it happens. A short note in the engineering channel describing this campaign, with the specific pattern of Slack recruitment plus a runnable take-home, is more effective than a general training module, because it gives people a concrete script to recognise. Developers are good at pattern matching once they know the pattern exists.

Then check the assumption that makes it costly. Audit what a compromised developer workstation reaches: which cloud roles, which repositories, which registries, and how long those credentials live. Most organisations find long-lived tokens with broad scope, granted during onboarding and never revisited. Moving those to short-lived, scoped credentials is work that pays out against every workstation compromise, whether it arrives through a fake recruiter, a malicious editor extension, or an ordinary phishing email.

The recruiting angle will change as it becomes better known. The underlying request will not, because “run this code” is what the job involves, and any attacker who can make that request in a plausible context has most of what they need.