Skip to content

feat(security): SEC014/SEC015/SEC016 — second B-class batch#8

Merged
wei9072 merged 4 commits into
mainfrom
feat/sec-b-class-batch-2
May 6, 2026
Merged

feat(security): SEC014/SEC015/SEC016 — second B-class batch#8
wei9072 merged 4 commits into
mainfrom
feat/sec-b-class-batch-2

Conversation

@wei9072
Copy link
Copy Markdown
Owner

@wei9072 wei9072 commented May 6, 2026

Summary

Three more B-class rules following the α/β/γ pattern split from
PR #7. Each closes a real false-negative class observed in the
agent comparison study, all kept under FP rate < 1% via narrow
disambiguator filters.

Pattern Rule Description
α text-level SEC014 hardcoded -----BEGIN ... PRIVATE KEY----- PEM block
β multi-language SEC015 silent broad except — Python except Exception: pass, JS catch (e) {}
β multi-language SEC016 SSRF marker — HTTP call where URL arg is request input

Commits (review in order)

  1. feat(security): SEC014 (α) — hardcoded PEM private key
    text scan; public keys excluded.
  2. feat(security): SEC015 (β) — silent broad except / empty catch
    AST on except_clause (Python) / catch_clause (JS / TS).
    Body-emptiness logic mirrors the existing empty_handler_count
    signal in signals/smells.rs. Bare except: stays SEC013's job.
  3. feat(security): SEC016 (β) — SSRF marker on HTTP call
    per-language receiver gating filters dict.get / cache.get
    noise. URL-arg pattern list covers Flask / Django / FastAPI /
    Express / Koa request shapes.
  4. docs(readme): bump SEC rule count 13 → 16 + name new rules
    EN + ZH README tables.

Skipped (deferred or out of scope)

These were on the backlog but excluded from this PR because they
can't hit FP rate < 1% without dataflow analysis:

  • Path traversal (../ in path-like contexts) — too many legit
    relative imports / paths.
  • IDOR (/api/x/<id> without owner check) — needs route + auth
    context.
  • N+1 query (loop with DB call inside) — needs DB call awareness.
  • Float for money (variable named price/amount) — heuristic
    too broad, lots of legit non-money use.

If we later add a dataflow / type pass, those become viable.

Test results

cargo test --workspace — 130 / 130 pass (was 116 baseline; +14
new tests covering positive and must-NOT-fire negatives per rule).

Live MCP sanity-check on a single Python file with all 3 patterns:

$ python3 aegis_validate.py /tmp/b2-test.py
FINDINGS ...: 20 total (security=3, signal=16, workspace=1)
  [security] SEC014 @line 3   hardcoded PEM private key ...
  [security] SEC015 @line 13  broad exception caught and silently swallowed ...
  [security] SEC016 @line 8   outbound HTTP call with URL derived from request input ...

All three patterns were silent before this PR.

Test plan

  • cargo test --workspace — 130 / 130 pass
  • cargo install --path crates/aegis-mcp --force succeeds
  • Live MCP run fires SEC014 + SEC015 + SEC016 on test file
  • False-positive guards verified by negative tests:
    • -----BEGIN PUBLIC KEY----- → no SEC014
    • except ValueError: pass → no SEC015 (specific type, narrow)
    • except Exception as e: log(e); raise → no SEC015 (body has work)
    • requests.get("https://api.example.com/data") → no SEC016 (static URL)
    • data.get(req.params.url) → no SEC016 (dict, not HTTP client)
  • CI green on push

🤖 Generated with Claude Code

wei9072 and others added 4 commits May 6, 2026 01:54
Cross-language text scan for `-----BEGIN ... PRIVATE KEY-----` in
any source file. α-class — language-agnostic, runs on every file
regardless of grammar.

Precision is essentially perfect: the PEM header is unique enough
that no legitimate code embeds it as a string literal. Public keys
(`-----BEGIN PUBLIC KEY-----`) are intentionally excluded — they
aren't credentials.

Tests: 3 — Python triple-string positive, JS escaped-string
positive, public-key negative.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Catches the slightly more sophisticated form of the bare-except
antipattern: agent picked an exception type but chose the broadest
one and then silently swallowed it.

Two language shapes:

- **Python**: `except Exception: pass` or `except BaseException: pass`
  — broad type, body is `pass` only / empty / single comment / a
  bare `return` / `return None`.
- **JavaScript / TypeScript**: `catch (e) {}` or `catch (e) { /* nothing */ }`
  — catch is implicitly broad in JS, so any catch with an empty /
  comment-only body fires.

Excludes (FP control):
- Bare `except:` — already SEC013's territory
- Specific types: `except ValueError: pass` — developer expected
  this case
- Body that logs / re-raises / does anything substantive

Body-emptiness logic mirrors `signals/smells.rs::handler_body_is_empty`
so behaviour stays consistent with the existing
`empty_handler_count` signal.

Tests: 6 — Python broad+pass / Python broad+log negative / Python
specific-type negative / Python BaseException positive / JS empty
positive / JS log+throw negative.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… URL

AST rule on call expressions. Fires when an outbound HTTP call
(`requests.get`, `httpx.X`, `aiohttp.X`, `urllib.urlopen`, `fetch`,
`axios.X`, `http.Get`) is given a URL argument that obviously came
from request input.

Per-language receiver gating keeps `dict.get()` / `cache.get()` /
`Map.get()` from firing — only http-shaped receivers count.

User-input shapes (conservative needle list):
- Flask / Django / FastAPI: `request.args`, `request.form`,
  `request.json`, `request.values`, `request.params`
- Express / Koa: `req.body`, `req.params`, `req.query`,
  `req.headers`, `ctx.request`, `ctx.params`
- Subscript access: `params[...]`, `query[...]`, `body[...]`
- Generic: `user_input`, `user_url`, `input(...)`

Tests: 5 — Python `requests.get(req.params.url)` positive, Python
`request.args.get('url')` positive, JS `fetch(req.query.url)`
positive, static URL negative, `dict.get(req.params.url)` negative
(receiver-name disambiguator).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@wei9072 wei9072 merged commit ef3c179 into main May 6, 2026
1 check passed
@wei9072 wei9072 deleted the feat/sec-b-class-batch-2 branch May 6, 2026 01:58
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