Skip to content

fix(scan): stop reducing scoped packages to the part after the slash - #85

Merged
ralyodio merged 1 commit into
masterfrom
fix/typosquat-scoped-packages
Aug 10, 2026
Merged

fix(scan): stop reducing scoped packages to the part after the slash#85
ralyodio merged 1 commit into
masterfrom
fix/typosquat-scoped-packages

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The bug

detectTyposquat stripped the scope before comparing a dependency name against the popular-package list:

const lower = name.toLowerCase().replace(/^@[^/]+\//, '');

So @capacitor/core became core, and editDistance("core", "cors") === 1 — reported as a typosquat of cors at high severity.

This is not a one-package quirk. Every scoped package named core hits it:

dependency reported as
@capacitor/core one edit from cors
@babel/core one edit from cors
@angular/core one edit from cors
@nestjs/core one edit from cors
@sentry/core one edit from cors

Any scoped package whose bare name sits one edit from something popular takes the same path (@some-org/expresexpress). The rule's loudest finding was landing on some of the most widely installed packages in the ecosystem.

The fix

Scoped names are no longer candidates for this check.

Scopes are owned. Publishing @babel/anything requires control of the @babel scope, so a squat cannot be planted inside a legitimate one, and nobody typing npm i cors arrives at @capacitor/core. The misreading this rule models — a name you skim past in a diff — does not cross the scope boundary.

No detection is lost

Stripping the scope never compared scopes at all, only the segment after the slash. A lookalike scope (@babeljs/core for @babel/core) was uncaught before this change and is uncaught after it. Catching that needs a list of popular scopes to compare against, which is a separate check rather than a variation on this one — noted in a comment for whoever picks it up.

Verification

Against ionic-team/capacitor at 5e5bb3b, same tree, same invocation: 21 findings → 19. Both manifest-typosquat reports removed, nothing added, no other finding perturbed.

Three tests added. Two of them fail on master with exactly the assertions above (cors for @capacitor/core, express for @some-org/expres); the third pins the unscoped case (corescors) so the exemption can't become a way to smuggle a bare name past the check.

vitest run in apps/cli: 65 passed. tsc --noEmit: clean.

detectTyposquat stripped the scope before comparing against the popular
package list, so `@capacitor/core`, `@babel/core`, `@angular/core`,
`@nestjs/core` and `@sentry/core` all reduced to `core` — one edit from
`cors` — and were reported as typosquats at high severity. Any scoped
package whose name sat one edit from something popular hit the same path,
which put the rule's loudest finding on some of the most widely installed
packages in the ecosystem.

A scope is owned: publishing `@babel/anything` requires control of the
`@babel` scope, so a squat cannot be planted inside a legitimate one, and
nobody typing `npm i cors` arrives at `@capacitor/core`. The misreading
this rule models does not cross the scope boundary, so scoped names are
no longer candidates.

No detection is lost. Stripping the scope never compared scopes at all,
only the segment after the slash, so a lookalike scope (`@babeljs/core`
for `@babel/core`) went uncaught before and still does — catching it
needs a list of popular scopes, which is a separate check.

Verified against ionic-team/capacitor: 21 findings to 19, removing both
manifest-typosquat reports and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

144 finding(s)

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

Severity Rule Location
HIGH sql-template-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:30
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:182
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:20
MEDIUM sql-template-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:34
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:39
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:48
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:52
MEDIUM js-shell-exec-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:64
MEDIUM js-unsafe-yaml-load apps/cli/src/scan/__tests__/code-rules.test.ts:178
MEDIUM sql-template-interpolation apps/extension/scripts/build.js:320
MEDIUM sql-template-interpolation apps/extension/scripts/build.js:326
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:180

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 84c65aa into master Aug 10, 2026
10 checks passed
ralyodio added a commit that referenced this pull request Aug 10, 2026
…ge guard (#88)

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>
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.

1 participant