Skip to content

fix(scan): stop letting neighbouring code decide a finding's severity (v0.7.1) - #97

Merged
ralyodio merged 1 commit into
masterfrom
fix/inherent-severity
Aug 11, 2026
Merged

fix(scan): stop letting neighbouring code decide a finding's severity (v0.7.1)#97
ralyodio merged 1 commit into
masterfrom
fix/inherent-severity

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Release 0.7.1. Fixes the severity inconsistency I flagged on #95.

The symptom

Eight findings on ralyodio/debtap, all the same class — a package source fetched without a trustworthy channel — reported at two different severities:

103   high      contextual  sh-insecure-transport-flag
104   high      contextual  sh-plaintext-download
106   high      contextual  sh-plaintext-download
108   high      contextual  sh-plaintext-download
111   high      contextual  sh-plaintext-download
113   medium    pattern     sh-plaintext-download
120   medium    pattern     sh-insecure-transport-flag
128   medium    pattern     sh-insecure-transport-flag

Nothing distinguishes line 113 from line 111 except distance from line 103. A --fail-on high gate would have caught five of eight identical problems and let three through — which is worse than catching none, because it looks like it worked.

Cause 1: an awk program read as untrusted input

Line 103 is:

version=$(curl -k -s https://packages.ubuntu.com | gawk -F '=' '{print $2}' | gawk '{print $1}' | ...)

$2 and $1 there are awk field references inside single-quoted programs. The shell performs no expansion inside single quotes, so they are literally the characters $2 and $1 — but the shell untrusted-input pattern matched them as positional parameters. That escalated line 103 to contextual, and the ±6-line guard window spread the escalation to 104–111.

Single-quoted spans are now blanked before the untrusted-input test, for shell only. target="$1" — a real positional parameter — still escalates, which is pinned by its own test.

Cause 2: these rules should never have asked

The confidence cap exists for injection: exec(cmd) is a construct, and only becomes a vulnerability once cmd can be influenced, so capping it at medium without visible untrusted input is correct and valuable.

It is wrong wherever the construct is the defect. curl -k against HTTPS is interceptable whatever surrounds it. DES is broken in every file that uses it. Nothing six lines away changes the finding text, so nothing six lines away should change its severity.

Those rules now carry inherent, reporting at confidence evidence and their declared severity — the same treatment the credential rules already get, and for the same stated reason: a committed AWS key is a committed AWS key.

rule CWE
tls-verification-disabled CWE-295
sh-insecure-transport-flag CWE-295
sh-remote-script-execution CWE-494
sh-plaintext-download CWE-319
sh-world-writable-permissions CWE-732
java-broken-cipher CWE-327

This is not a blanket escalation. Context-dependent rules still cap, and there is a test on js-unescaped-html-sink asserting exactly that, so the field cannot quietly become a way to make a rule look important.

Result

103   high      evidence    sh-insecure-transport-flag
104   high      evidence    sh-plaintext-download
…
128   high      evidence    sh-insecure-transport-flag

Same eight findings — none added, none lost — now uniform.

Verification

  • debtap: 8 findings, unchanged set, uniformly high/evidence.
  • capacitor: 26 findings, identical set, every severity unchanged — the fix does not reach rules that legitimately depend on context.
  • 123 tests, up from 118. Each new test was confirmed to fail with its half of the fix reverted: blanking removed → the awk case fails; inherent removed → both inherent cases fail.
  • tsc --noEmit clean.

Release 0.7.1.

On ralyodio/debtap the same defect reported two different severities:

  103  high    contextual  sh-insecure-transport-flag
  113  medium  pattern     sh-plaintext-download
  120  medium  pattern     sh-insecure-transport-flag

Eight identical insecure-fetch findings, split by nothing more than distance
from line 103. A --fail-on high gate would have caught five and let three
through.

Two causes, both fixed.

An awk field reference was being read as a shell positional parameter. Line
103 contains gawk -F '=' '{print $2}', and the shell expands nothing inside
single quotes, so that $2 is two characters, not untrusted input. It
escalated confidence to contextual, and the six-line guard window carried the
escalation to four neighbours. Single-quoted spans are now blanked before the
untrusted-input test, for shell only.

Separately, these rules should never have consulted context at all. Capping
to medium without visible untrusted input is right for injection — exec(cmd)
becomes a vulnerability once cmd can be influenced — and wrong where the
construct is itself the defect. curl -k against HTTPS is interceptable
whatever surrounds it; DES is broken in every file that uses it. Those rules
are marked inherent and report at confidence evidence and their declared
severity, the same treatment the credential rules already get:
tls-verification-disabled, sh-remote-script-execution,
sh-insecure-transport-flag, sh-plaintext-download,
sh-world-writable-permissions, java-broken-cipher.

Not a blanket escalation — context-dependent rules still cap, pinned by a
test on js-unescaped-html-sink.

debtap: same 8 findings, now uniformly high/evidence. capacitor: 26 findings
with every severity unchanged. 123 tests, up from 118; each new one confirmed
to fail with the fix reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

157 finding(s)

HIGH/CRITICAL: 14 | MEDIUM: 107 | LOW: 36

Severity Rule Location
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 sql-template-interpolation packages/scan/src/__tests__/code-rules.test.ts:31
HIGH secret-aws-access-key packages/scan/src/secret-rules.ts:192
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
HIGH sh-eval-expansion .githooks/pre-commit:26
HIGH secret-generic-credential modules/spend-guard/config/example.conf.toml:13
HIGH secret-generic-credential modules/spend-guard/README.md:84
HIGH js-unsafe-yaml-load packages/scan/src/__tests__/code-rules.test.ts:216
HIGH secret-generic-credential PRD.md:268
HIGH sh-remote-script-execution scripts/smoke-test.sh:46
HIGH sh-remote-script-execution scripts/smoke-test.sh:47
MEDIUM insecure-temp-file .githooks/commit-msg:16
MEDIUM insecure-temp-file .githooks/post-commit:20
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-template-interpolation apps/extension/scripts/build.js:320
MEDIUM sql-template-interpolation apps/extension/scripts/build.js:326
MEDIUM sh-remote-script-execution apps/web/public/install.sh:272
MEDIUM sh-remote-script-execution apps/web/public/install.sh:320
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:180
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:184

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

Snippets are redacted; ThreatCrush never prints matched credential material.

it('leaves context-dependent rules capped, so this is not a blanket escalation', () => {
// `js-unescaped-html-sink` is not inherent: a static assignment says
// nothing about attacker data, and it must still cap at medium.
const finding = findings('a.js', 'el.innerHTML = "<b>" + name + "</b>";')[0];
@ralyodio
ralyodio merged commit 422d8a9 into master Aug 11, 2026
10 checks passed
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