타이포스쿼트 패키지가 내 열쇠를 거의 손에 넣었다: 공격을 안전하게 해부하기

작성자

카테고리:

← 피드로
DEV Community · Pavel Espitia · 2026-07-26 개발(SW)

A recruiter DM led to a “take-home” repo. Standard stuff, or so it looked. I cloned it, opened package.json, and one line stopped me: a dependency named clx-cookieparser. The real package is cookie-parser. That extra clx- prefix and the missing hyphen were the whole attack. I never installed it. Here is how I read it apart without running a single line, and the checklist that has saved me twice now.

I do smart contract security, but the boring truth is most attacks against developers do not touch the chain at all. They touch your machine, your environment variables, and your wallet files. This one wanted all three.

The bait

The repo was framed as a coding assessment for a “client.” Clean README, a couple of real features, tests that passed. The kind of thing you skim and trust because it looks like work, not like a trap. That framing is the point. You are in “let me finish this task” mode, not “let me audit a stranger’s code” mode.

The dependency list had mostly normal packages. Express, a test runner, a couple of utilities. And then clx-cookieparser, sitting in the middle like it belonged. Typosquatting works because your eyes autocorrect. You read “cookie parser,” your brain fills in the canonical name, and you move on.

I did not move on, mostly out of habit. I keep npm configured so that installing is not a one-command reflex, which buys me time to look before anything executes.

Why I could read it without running it

Two settings do the heavy lifting here, and I recommend both to everyone:

# never run install/postinstall scripts automatically
npm config set ignore-scripts true

# with pnpm, add a cooldown so brand-new versions can't hit you instantly
pnpm config set minimumReleaseAge 1440   # 24 hours

Enter fullscreen mode Exit fullscreen mode

ignore-scripts matters because the classic move is a postinstall hook that fires the moment you run install. Turn that off and cloning plus reading is safe, because nothing runs on its own. The cooldown matters because a lot of these malicious versions get yanked within hours of publication once someone reports them, so a 24-hour delay quietly dodges the freshest poison.

So I read. Reading is not running. That distinction is the entire safety model.

What the payload was shaped like

I want to be careful here, so I am describing structure, not handing anyone a recipe. No real payload code.

The package had two layers. The first layer, the code visible in the published tarball, was almost boring. It wrapped a real cookie-parsing function so the thing actually worked if you used it. That is camouflage. If the library breaks your app, you rip it out and the attacker loses. If it works, you keep it and stop looking.

The interesting part was a second dependency the first package pulled in, and here is the tell that made my neck prickle: that second dependency was present in the resolved dependency tree but did not appear in the lockfile the repo shipped. In other words, the code referenced a package that the committed lockfile did not account for. A supply chain that does not reconcile with its own lockfile is lying to you about something.

That second-stage package is where the real behavior lived. Reading its structure (not executing it), the intent was obvious from the surface it reached for:

  • It looked for environment variables and process env, the place people stash API keys, tokens, and RPC URLs.
  • It probed common wallet and keystore file locations in the home directory.
  • It assembled that data and prepared to ship it outbound to a remote endpoint.

Two-stage is a deliberate design. Stage one is quiet and passes a casual glance. Stage two carries the theft and hides behind an install-time or first-run trigger, one step removed from the package you actually named in your file. You have to follow the thread to see it.

The signals that gave it away

I did not need a fancy tool for the first pass. I needed to read like the code was guilty until proven innocent. The signals, roughly in the order they hit me:

  1. The name. clx-cookieparser is not cookie-parser. Any prefix, swapped hyphen, or singular/plural flip on a popular package name is a five-alarm reason to stop.
  2. Lockfile mismatch. A dependency resolving in the tree that the committed lockfile does not describe means the manifest and reality disagree. Legitimate projects reconcile.
  3. Obfuscation and entropy. The second stage had chunks of high-entropy strings, the dense base64-looking blobs and hex that normal utility code just does not carry. Human-written cookie parsing is low entropy and readable. A wall of encoded bytes is a place to hide behavior from a reader.
  4. Reach that does not match purpose. A cookie parser has no business reading your home directory or touching env beyond what it is handed. Capability that exceeds the stated job is intent.

For the entropy check you do not need to run anything either. You can eyeball it, or score strings statically. A rough sketch of the idea:

import math
from collections import Counter

def shannon_entropy(s: str) -> float:
    if not s:
        return 0.0
    counts = Counter(s)
    n = len(s)
    return -sum((c / n) * math.log2(c / n) for c in counts.values())

# high entropy long string literals in a "utility" package are a smell
for line in open("suspicious.js"):
    for token in line.split('"'):
        if len(token) > 120 and shannon_entropy(token) > 4.5:
            print("high-entropy blob:", token[:40], "...")

Enter fullscreen mode Exit fullscreen mode

That is reading, scoring, and flagging. Never executing.

The checklist that saved me

This is the routine now, and it takes maybe five minutes on a fresh repo:

  • Read package.json dependencies out loud. Any name that is close-but-not-exact to a popular package gets verified against the real registry name before anything else.
  • Diff the dependency tree against the lockfile. Anything present in one and missing from the other is a stop sign.
  • Never let install scripts run by default. ignore-scripts true globally, opt in per project only when you trust it.
  • Grep for high-entropy string literals and unexpected network or filesystem calls in dependencies before install, not after.
  • Assume any repo from an unsolicited recruiter is hostile until proven otherwise. The social framing is part of the exploit.

I ended up feeding the repo through Argus Lens, the static repo scanner I built (lens.noctis.biz) partly because I got tired of doing this by hand. It flags build-time execution, lockfile-missing dependencies, and obfuscation without cloning or installing. But the tool just automates the checklist above. The checklist is the actual defense, and you can run it with your eyes.

I did not lose anything this time. The uncomfortable part is how close normal, get-the-task-done behavior came to running it. One npm install on autopilot and I would have shipped my env vars to someone.

What is your default posture when a stranger sends you a repo to run: trust and clone, or hostile until proven safe?

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다