Skip to content

feat(security): SEC004 + SEC007 multi-language dispatch (PR #18)#18

Merged
wei9072 merged 1 commit into
mainfrom
feat/sec3-8-audit
May 7, 2026
Merged

feat(security): SEC004 + SEC007 multi-language dispatch (PR #18)#18
wei9072 merged 1 commit into
mainfrom
feat/sec3-8-audit

Conversation

@wei9072
Copy link
Copy Markdown
Owner

@wei9072 wei9072 commented May 7, 2026

Summary

Closes the audit gaps in SEC003-008 noted after PRs #10 / #12.
Two rules had real coverage gaps; the others were already decent.

Rule Coverage before Action
SEC003 TLS off text Python / Node / Go / .NET ☑ unchanged
SEC004 shell Python `shell=True` only ✅ language-aware dispatch
SEC005 SQL concat Python / Java decent ☑ unchanged
SEC006 CORS text cross-language ☑ unchanged
SEC007 JWT `name.contains("jwt")` — Python only ✅ language-aware dispatch
SEC008 deser Python / Node / Java decent ☑ unchanged

SEC004 expansion

Language Catch shape
Python subprocess.run / Popen with `shell=True` + interp
Node.js `child_process.exec` / `execSync` + interp (always shells)
PHP global `shell_exec` / `exec` / `passthru` / `system` / `proc_open` + interp
Java `Runtime.exec(String)` overload + concat (String[] overload safe)
Go `exec.Command("sh", "-c", ...)` + interp; bare `exec.Command("ls", arg)` safe

`text_has_interp` extended with PHP `.` concat (gated on `$` to
avoid floating-point literals).

SEC007 expansion

Language Catch shape
Python `jwt.decode(...)` without algorithms/key/verify (existing)
Node.js `jsonwebtoken.decode()` always unverified; `verify()` with `verify:false` opt
Java / Kotlin `JWT.decode(token)` returns unverified DecodedJWT (Auth0 lib)
PHP `JWT::decode` without explicit algorithm list

`check_jwt_unsafe` now takes `&ParsedFile` so language identity
is available — prevents PHP `JWT::decode` from being misclassified
as Java (the old `name.contains("JWT")` check was language-blind).

Infrastructure

  • `call_name` extended for PHP: now handles
    `scoped_call_expression` (`Class::method`) and
    `member_call_expression` (`$obj->method`). Previously these
    fell back to `named_child(0)` and returned just the receiver.
  • `leaf_method_name(name)` helper splits on `.` / `::` /
    `->` so leaf-method matching works across all four conventions.
  • walk dispatch picks up `scoped_call_expression` +
    `member_call_expression` node kinds.

Tests

10 new tests (5 SEC004 + 5 SEC007 covering Python / Node / PHP /
Java / Go). 174 → 177 total tests pass.

Test plan

  • `cargo test --workspace` — 177 / 177 pass
  • PHP scoped + member call shapes recognised by `call_name`
  • No language-blind FP between Java JWT and PHP JWT
    (each only fires on its own language)
  • Bare `exec.Command("ls", arg)` Go pattern stays silent
    (positive negative)
  • `child_process.exec('ls -la')` static-string stays silent
  • CI green on push

🤖 Generated with Claude Code

Closes the audit gaps identified after PRs #10 / #12. The audit
across SEC003-008:

| Rule | Coverage before | Action this PR |
|---|---|---|
| SEC003 TLS off  | text-level Python/Node/Go/.NET | unchanged (decent already) |
| SEC004 shell    | Python `shell=True`+interp only | **language-aware dispatch** |
| SEC005 SQL concat | text+string-literal Python/Java | unchanged (decent) |
| SEC006 CORS     | text-level cross-language | unchanged |
| SEC007 JWT      | `name.contains("jwt")` Python only | **language-aware dispatch** |
| SEC008 deser    | Python/Node/Java idioms | unchanged (decent) |

## SEC004 expansion

Per-language shell-running idioms; requires interpolation in arg.

- **Python**: subprocess.run/Popen with `shell=True` + interp
- **Node.js**: `child_process.exec` / `execSync` with interp (always
  shells out, no `shell:true` gate; `execFile` is the safe one)
- **PHP**: global `shell_exec` / `exec` / `passthru` / `system` /
  `proc_open` with interp
- **Java**: `Runtime.getRuntime().exec(String)` overload with
  concat — String[] overload safe and excluded
- **Go**: `exec.Command("sh"|"bash"|"/bin/sh"|"/bin/bash", "-c", ...)`
  with interp. Bare `exec.Command("ls", arg)` (argv-style) excluded
  — no shell metachar interpretation

`text_has_interp` extended with PHP `.` concat (gated on `$` to
avoid floating-point literals).

## SEC007 expansion

Per-language JWT decode without verification:

- **Python**: `jwt.decode(...)` without algorithms/key/verify kwarg
  (existing behaviour)
- **Node.js**: `jsonwebtoken.decode()` always returns unverified
  claims — flag unconditionally; `verify(token, secret, opts)` is
  the safe API. `verify()` with `verify: false` opt also flagged.
- **Java / Kotlin**: Auth0 lib's `JWT.decode(token)` returns
  unverified DecodedJWT; safe path is
  `JWT.require(...).build().verify(token)`.
- **PHP**: firebase/php-jwt's `JWT::decode($token, $key)` requires
  explicit algorithm list. Flagged unless one of `'HS256'`/`'RS256'`/
  `'ES256'`/`'EdDSA'` appears in call text.

Algorithm-`none` detection extended with JWT-spec literal
`"alg": "none"` shape.

`check_jwt_unsafe` now takes `&ParsedFile` so language identity
is available — prevents PHP `JWT::decode` from being misclassified
as Java (the old `name.contains("JWT")` check was language-blind).

## Infrastructure changes

1. **`call_name` extended for PHP scoped/member calls.** Previously
   only handled Java's `method_invocation`; now also composes
   `Class::method` from `scoped_call_expression` and
   `$obj->method` from `member_call_expression`.
2. **`leaf_method_name(name)` helper** — splits on `.` / `::` /
   `->` so `JWT::decode`'s leaf is `decode`, not the whole string.
3. **walk dispatch** extended with `scoped_call_expression` and
   `member_call_expression` node kinds.

## Tests

10 new (5 SEC004 multi-lang + 5 SEC007 multi-lang). 174 → **177**
total tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@wei9072 wei9072 merged commit 66224e1 into main May 7, 2026
1 check passed
@wei9072 wei9072 deleted the feat/sec3-8-audit branch May 7, 2026 08:41
wei9072 added a commit that referenced this pull request May 7, 2026
…#19) (#19)

Continues PR #18's audit. SEC003/005/008 already had decent
multi-language coverage, but each had specific gaps that surface
in real-world auth / DB / serialization code.

## SEC003 — TLS verification disabled

Added detection for:

- **PHP curl**: \`CURLOPT_SSL_VERIFYPEER => false / 0\` and
  \`CURLOPT_SSL_VERIFYHOST => 0\`. Both positional
  (\`curl_setopt($ch, OPT, 0)\`) and inline-array
  (\`curl_setopt_array($ch, array(OPT => false))\`) forms.
  Deferred-via-variable form intentionally not caught (would need
  dataflow).
- **Ruby OpenSSL**: \`VERIFY_NONE\` / \`OpenSSL::SSL::VERIFY_NONE\`.
- **Java**: \`NoopHostnameVerifier\` / \`TrustAllHostnameVerifier\`
  class names. Doesn't catch arbitrary anonymous-inner-class trust
  managers — those need real flow analysis.

## SEC005 — SQL string concat

Receiver-method matcher expanded:

| Family | Methods added |
|---|---|
| Python | (existing: execute, executemany) |
| JDBC | + executeLargeUpdate, executeBatch |
| Node.js / Go | + Query, QueryRow, QueryContext, Exec, ExecContext |
| PHP PDO | + exec, prepare |
| Ruby | + find_by_sql |
| PHP global | + mysqli_query, mysql_query, pg_query, sqlite_query |

Plus:
- \`leaf_method_name\` used (handles PHP \`::\` / \`->\`).
- \`text_has_interp\` (introduced in PR #18) replaces SEC005's
  Python-shaped local check, picking up PHP \`.\` concat correctly.
- \`contains_sql_in_string_literal\` extended to recognize PHP's
  \`encapsed_string\` / \`string_value\` and heredoc / template
  literal kinds, so the SQL-keyword gate sees the string content.

## SEC008 — insecure deserialization

Dangerous-call list extended:

| Language | Added |
|---|---|
| Python | + shelve.open, dill.loads, dill.load |
| Java | + XMLDecoder.readObject |
| **PHP** | + unserialize (textbook unsafe path) |
| **Ruby** | + Marshal.load, YAML.load, Oj.load |
| **C# / .NET** | + BinaryFormatter.Deserialize, SoapFormatter.Deserialize, NetDataContractSerializer.ReadObject |
| Go | + gob.NewDecoder |

## Tests

7 new (SEC003 PHP curl ×3, SEC005 Go db.Query + PHP mysqli_query,
SEC008 PHP unserialize + C# BinaryFormatter). 184 → 191 total
tests pass.

Co-authored-by: Claude Opus 4.7 <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