Skip to content

feat(scan): shebang scripts, shell and PHP rule sets, language-coverage guard (v0.6.0) - #88

Merged
ralyodio merged 1 commit into
masterfrom
release/v0.6.0
Aug 10, 2026
Merged

feat(scan): shebang scripts, shell and PHP rule sets, language-coverage guard (v0.6.0)#88
ralyodio merged 1 commit into
masterfrom
release/v0.6.0

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Release 0.6.0.

Recovers work stranded by the stack merge

#85, #86 and #87 were stacked. #85 merged to master; #86 and #87 merged into their intermediate base branches, so their content never reached mastermaster today has neither fileRequires nor shebang detection nor the shell rules. This branch is cut from master and carries all of it.

Two languages had no rules

shell and php were both in ScanLanguage, both mapped from extensions, and between them zero of the 46 code rules targeted either. From the outside they looked supported: files were read, matched against the secret rules, and reported clean whatever the code did.

Detection made it worse — extension-only, so an executable named for the command it provides rather than the language it is written in was never opened at all:

$ threatcrush scan debtap        # 3,511 lines of bash
✔ Scanned 0 files
✓ No security issues found!

A clean scan that scanned nothing is the one result a security tool must never produce.

What's in it

  • Shebang detection for extensionless files, read from a 128-byte prefix so an extensionless blob costs one small read rather than a megabyte decoded as UTF-8 and thrown away. Unrecognised extensions are still skipped — .png is not a script.
  • Seven shell rulescurl | bash (CWE-494), eval on an expansion (CWE-78), unquoted expansion in rm -rf (CWE-78), disabled cert verification (CWE-295), plain-HTTP download (CWE-319), chmod 777 (CWE-732), predictable temp paths (CWE-377).
  • Eight PHP rules — SQL interpolation (CWE-89), shell interpolation (CWE-78), eval (CWE-95), dynamic include (CWE-98), unserialize on request data (CWE-502), unescaped output (CWE-79), request-driven path traversal (CWE-22), extract() variable injection (CWE-621).
  • A language-coverage test. Both gaps existed because nothing checked the language list against the rule list. Now a test fails when a language the scanner claims has no rule targeting it.

Every rule is built against the corrected shape as well as the vulnerable one, per this file's existing contract: rm -rf "$DIR" doesn't match, escapeshellarg($f) doesn't match, include basename($p) doesn't match, a prepared statement doesn't match, curl … | jq doesn't match.

The eval rule matches eval's argument, not the line

The obvious spelling, \beval\b.*\$, is wrong. Bash builds a numeric range with

for r in $(eval echo {$(($k + 1))..$(($k + $n - 1))}); do

Every expansion sits inside $((…)), which the shell parses as an arithmetic expression — a ; there is a syntax error, not a second command. A line-wide search still finds $k inside it and fires: 355 findings in debtap alone, all one safe loop. Anchoring to the argument keeps eval "$cmd" and drops the idiom.

Verification

  • debtap: 0 findings (nothing scanned) → 8, all genuine. Lines 120 and 128 are the sharp ones — an archive from GitHub and the AUR package list fetched over HTTPS with curl -k, written to /var/cache/debtap/, then used to build packages.
  • capacitor at 5e5bb3b: unchanged at 13. PHP and shell rules fire on none of it; shebang detection adds no files.
  • 100 tests, up from 68. tsc --noEmit clean.

Tuning was measured, not guessed: debtap went 368 → 13 (eval anchored) → 8 (plain-HTTP double-report removed).

After merge

Tag v0.6.0 to publish. ralyodio/debtap#1 pins 0.6.0 and is red until then.

…ge guard

Release 0.6.0.

Two languages the scanner claimed to support had no rules at all. `shell`
and `php` were both in `ScanLanguage`, both mapped from extensions, and
between them zero of the 46 code rules targeted either. From the outside
they looked supported: files were read, matched against the secret rules,
and reported clean whatever the code did.

Language detection made it worse — it was extension-only, so an executable
named for the command it provides rather than the language it is written in
was never opened. `ralyodio/debtap`, 3,511 lines of bash in a file called
`debtap`, scanned clean by scanning nothing and exited 0 while doing it.

- Shebang detection for extensionless files, from a 128-byte prefix so a
  checked-in blob costs one small read rather than a megabyte decoded and
  discarded.
- Seven shell rules: remote script execution, eval on an expansion,
  unquoted expansion in a recursive remove, disabled certificate
  verification, plain-HTTP download, world-writable permissions,
  predictable temp paths.
- Eight PHP rules: SQL interpolation, shell interpolation, dynamic code
  execution, dynamic include, unserialize on request data, unescaped
  output, request-driven path traversal, extract() variable injection.
- A language-coverage test that fails when a language the scanner claims
  has no rule targeting it. Both gaps above existed because nothing checked
  the two lists against each other; now something does.

Also carries the two fixes stranded on intermediate branches when the
stack was merged out of order: scoped packages are no longer reduced to
the part after the slash (#85), and XXE requires XML evidence in the file
(#86).

Every rule is built against the corrected shape as well as the vulnerable
one. The eval rule matches eval's argument rather than the whole line: the
line-wide form counts bash's dynamic-range idiom, where every expansion is
arithmetic and cannot carry a command, and reported it 355 times in debtap
alone.

debtap: 0 findings (nothing scanned) to 8, all genuine.
capacitor: unchanged at 13. Tests: 100, up from 68.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
});

it('flags dynamic code execution', () => {
expect(ruleIds('a.php', 'eval($code);')).toContain('php-dynamic-code-execution');

it('flags dynamic code execution', () => {
expect(ruleIds('a.php', 'eval($code);')).toContain('php-dynamic-code-execution');
expect(ruleIds('a.php', 'eval("return $expr;");')).toContain('php-dynamic-code-execution');
* line of every installer.
*/
const UNTRUSTED_SH =
/\$\{?[1-9]\d*\b|\$[@*]|\$\{@\}|\bread\s+(?:-\S+\s+)*[A-Za-z_]\w*|\$\{?REPLY\b|\$\{?QUERY_STRING\b/;
languages: ['shell'],
// The pipe must be the *next* thing: `curl -o f url && sh f` is a different
// (and checkable) shape, and `curl url | jq` is not an execution at all.
pattern: /\b(?:curl|wget)\b[^|\n]*\|\s*(?:sudo\s+(?:-\S+\s+)*)?(?:\/bin\/|\/usr\/bin\/)?(?:ba|da|k|z|a)?sh\b/,
// actually re-parses untrusted text as source. `eval echo $x` is not
// covered; catching it without also catching the range idiom needs to know
// which expansions are arithmetic, which is parsing, not matching.
pattern: /\beval\s+(?:-\S+\s+)*(?:"\s*)?\$(?:\{?[A-Za-z_]\w*|\((?!\())/,
// never reaches the `$` and never matches. Only a genuinely bare expansion
// does. Restricted to recursive/forced removal: a bare `$f` in `rm $f` is
// sloppy, but it is not the shape that erases a filesystem.
pattern: /\brm\s+(?:-[a-zA-Z-]*[rRf][a-zA-Z-]*\s+)+[^"'\n]*?\$\{?[A-Za-z_]/,
cwe: 'CWE-732',
severity: 'medium',
languages: ['shell'],
pattern: /\bchmod\s+(?:-[a-zA-Z-]+\s+)*(?:0?777|a\+rwx|ugo\+rwx|a=rwx)\b/,
// Redirection or an explicit write into a literal `/tmp` path. A `$$` or
// `$RANDOM` suffix is still predictable, so it is not treated as a fix;
// `mktemp` is, and it is the guard below.
pattern: /(?:>{1,2}\s*|\b(?:tee|touch|cp|mv|install)\s+(?:-\S+\s+)*)\/tmp\/[\w.$-]+/,
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

152 finding(s)

HIGH/CRITICAL: 12 | MEDIUM: 104 | LOW: 36

Severity Rule Location
HIGH sql-template-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:31
HIGH secret-aws-access-key apps/cli/src/scan/secret-rules.ts:192
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:31
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:102
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:108
HIGH secret-aws-access-key modules/code-scanner/src/secrets/rules.ts:74
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
HIGH js-unsafe-yaml-load apps/cli/src/scan/__tests__/code-rules.test.ts:216
HIGH manifest-typosquat apps/mobile/package.json:43
HIGH secret-generic-credential modules/spend-guard/config/example.conf.toml:13
HIGH secret-generic-credential modules/spend-guard/README.md:84
HIGH secret-generic-credential PRD.md:268
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:70
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:79
MEDIUM sql-template-interpolation apps/cli/src/commands/properties.ts:226
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:88
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:111
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:121
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:125
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:31
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:33
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:34
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:35
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:36
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:43
MEDIUM sql-template-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:56
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:63
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:82
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:84
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:85
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:93
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:98
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:105
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:112
MEDIUM sql-template-interpolation apps/cli/src/index.ts:105
MEDIUM sql-template-interpolation apps/cli/src/index.ts:110
MEDIUM sql-template-interpolation apps/cli/src/index.ts:120
MEDIUM js-shell-exec-interpolation apps/cli/src/index.ts:411
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:21
MEDIUM sql-template-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:35
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:40
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:49
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:53
MEDIUM js-shell-exec-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:65
MEDIUM js-unsafe-yaml-load apps/cli/src/scan/__tests__/code-rules.test.ts:212
MEDIUM js-dynamic-code-execution apps/cli/src/scan/__tests__/code-rules.test.ts:471
MEDIUM js-dynamic-code-execution apps/cli/src/scan/__tests__/code-rules.test.ts:472
MEDIUM redos-nested-quantifier apps/cli/src/scan/code-rules.ts:140

…and 102 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 8d7dc15 into master Aug 10, 2026
9 checks passed
@ralyodio
ralyodio deleted the release/v0.6.0 branch August 10, 2026 17:32
// never reaches the `$` and never matches. Only a genuinely bare expansion
// does. Restricted to recursive/forced removal: a bare `$f` in `rm $f` is
// sloppy, but it is not the shape that erases a filesystem.
pattern: /\brm\s+(?:-[a-zA-Z-]*[rRf][a-zA-Z-]*\s+)+[^"'\n]*?\$\{?[A-Za-z_]/,
expect(ruleIds('i.sh', 'curl -fsSL https://example.invalid/i.sh | bash')).toContain(
'sh-remote-script-execution',
);
expect(ruleIds('i.sh', 'wget -qO- https://example.invalid/i.sh | su' + 'do sh')).toContain(
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants