Files
music_library/.pi/prompts/application-security-review.md
T
2026-05-13 19:29:11 +01:00

184 lines
8.8 KiB
Markdown

---
description: Perform an application-wide security review
---
You are a senior application security engineer. Audit the Elixir application in
the current working directory for real, exploitable vulnerabilities.
Use the tools available to you (Read, Grep, Glob, Bash) to explore the
codebase, follow data flow across modules, inspect call graphs, and check
commit history (`git log --all --grep`, `git log -S`) for unpatched variants
of past bugs. Spend effort proportional to the package's risk surface.
## Methodology
Two phases. Phase 1 is an inventory — write it down before judging anything.
Two runs against the same source should produce the same inventory.
### Phase 1: Boundaries + inventory
Before listing sinks, name the trust boundaries. For a small library this
is one or two lines: who calls it, what they pass, where external data
enters. Larger codebases get a table — actor, what they control, trusted
yes/no, where you found it documented. The per-sink boundary check in
Phase 2 references this list; it does not re-derive boundaries per sink.
Then enumerate every sink. For each: file, line, sink class, what it
consumes. Don't judge any of them yet — a sink is dangerous-if-input-is-
hostile, regardless of whether you currently think the input is hostile.
Grep exhaustively for the language's primitives in each class.
### Phase 2: Per-sink — six steps in order
Stop when a step rules the sink out and record which step did. Every
inventory sink ends up either in `findings` or in `ruled_out`.
1. Trace — backwards from sink to a boundary. Name each hop. If the
value never crosses a boundary, write "internal" and stop.
2. Boundary — which boundary from Phase 1 does it cross? The library
caller is not the attacker; documented config / operator-set values
are trusted unless the docs say otherwise. Cite the doc. Also: check
a precondition does not subsume the conclusion (an attack that
requires write access to a directory whose contents are documented
as executable is circular).
3. Validate — write a reproduction script. For Elixir, a short `.exs`
under `scripts/{package_name}/{short_description}.exs` runnable via
`Mix.install` is ideal. DO NOT execute it; the human will. Paste the
script in the `validation` field. For round-trip pairs, the script
runs `decode(encode(x))` and `encode(decode(s))` with structural
characters and shows the asymmetry.
4. Prior art — `git log --all --grep` and `git log -S` for the function
name and key strings; read closed issues/PRs; check whether the
behaviour is required by an RFC. If a maintainer already declined,
quote the comment.
5. Reach — for libraries: which kind of consumer would wire hostile
input here. You don't have dependents data; reason about plausible
call patterns. "No plausible exposed caller" is data, not a verdict.
6. Rate — severity + confidence. Critical = works on a fresh install,
no preconditions. High = realistic preconditions a normal deployment
satisfies. Medium = significant attacker positioning, unusual config,
or a chain. Low = unrealistic preconditions or narrow impact.
## Always-flag
Some sinks are dangerous enough on sight that the trace/boundary check is
skipped — flag every occurrence as a finding even if you can't trace where
the input comes from.
- **`:erlang.binary_to_term/1`, or `:erlang.binary_to_term/2` without
`:safe` in the options list.** Untrusted-binary deserialisation creates
arbitrary atoms (atom-table exhaustion DoS), can construct fun /
reference / pid terms that crash or hijack callers, and bypasses
parse-time validation entirely. The safe alternatives are
`:erlang.binary_to_term(bin, [:safe])` and
`Plug.Crypto.non_executable_binary_to_term/2`. Severity: **Critical**.
Report once per call site. If the same module also exposes the wrapper
that reaches the call site, mention the wrapper in the trace, but do
not skip the finding for lack of a traced caller.
- **`:erlang.binary_to_term/2` with `:safe`.** `:safe` blocks new atoms
and funs, but the decoded term is still attacker-shaped: deeply nested
structures cause memory amplification, existing atoms can still be
referenced (so any atom the BEAM has loaded is fair game), and callers
that pattern-match on a specific shape can crash or be confused. Worth
a note so reviewers can confirm the caller validates the result.
Severity: **Low**.
Sink classes — every place dangerous logic could live, regardless of whether
the input currently looks hostile. Enumerate first, judge second.
- Code execution — eval, dynamic dispatch on a computed name (`apply`,
`Code.eval_*`, `:erlang.apply/3` with computed args), code loaded from a
computed path, regex with embedded-code constructs.
- Command execution — `System.cmd`, `:os.cmd`, `Port.open({:spawn, …})`,
shelling out where args are built by concatenation rather than passed as
a list.
- File operations — `File.read/write/rm/cp/ln/chmod` where the path is
computed; `Code.require_file` / `Code.eval_file` with dynamic paths.
- Path handling — `Path.join/expand/relative_to`, traversal, symlink
following, case-fold confusion on case-insensitive filesystems.
- Archive extraction — `:erl_tar`, `:zip`, any unpack where entry names
become filesystem paths (zip-slip).
- Deserialisation — `:erlang.binary_to_term/1` (no `:safe`),
`Plug.Crypto.non_executable_binary_to_term/2` misuse, YAML/Marshal-style
formats that instantiate types during parse.
- Template / interpolation — values reaching another interpreted context
without escaping for it: HTML, SQL via raw fragments, EEx/Phoenix
`raw/1`, shell, regex, format strings, log lines.
- Network — clients that follow redirects, accept URLs from input, resolve
hostnames from data, TLS verification disabled (`verify: :verify_none`),
proxy handling.
- Validation — predicates whose contract is "this is safe": the sink is
the return value, the danger is returning the wrong answer.
- Cryptography — KDF parameters, IV reuse, mode/padding, MAC verification,
`==` on secrets instead of `Plug.Crypto.secure_compare/2`.
- Memory safety — Rust `unsafe`, raw pointers, unchecked indexing, FFI,
transmute. For NIFs: lifetime/aliasing across the BEAM boundary.
- Shared mutable state — `Application.put_env/3` from input, ETS/DETS,
`:persistent_term`, environment variables, signal handlers, Logger
backends. One input poisoning what another sees.
- Concurrency — check-then-act sequences a racer can interleave: file
existence before open, permission before access, GenServer state read
then written without serialisation.
- Resource consumption — atom leaks (`String.to_atom/1` on input),
unbounded loops/allocs, regex prone to catastrophic backtracking,
decompression with attacker-controlled ratio.
- Reflection / metaprogramming gadgets the library installs into the
caller — `__using__` macros, `@before_compile`, telemetry handler
attaches, Logger backends, monkeypatched callbacks. The library _chose_
to install the gadget; consumer wiring is a reach question, not a
reason to drop the sink.
- Round-trip integrity — pairs meant to be inverses: `encode`/`decode`,
`parse`/`serialize`, `marshal`/`unmarshal`. The sink is the pair. The
danger is asymmetry — if `decode(encode(x)) ≠ x`, or encode emits raw
what decode interprets, a value can change meaning across a store-and-
reload cycle and bypass parse-time validation on re-parse.
## Output
Always output the full report — boundaries and inventory must be present
even when nothing rises to a finding. Format:
## Trust boundaries
| Actor | Trusted | Controls | Source |
|-------|---------|----------|--------|
| <name> | yes/no/conditional | <what they control> | <doc citation> |
## Inventory
| ID | Location | Class | Consumes |
|----|----------|-------|----------|
| S1 | <rel/path>:<line> or <rel/path>:<line_start>-<line_end> | <sink class> | <what it consumes> |
## Findings
### F1 — <short title>
**Severity:** Critical | High | Medium | Low
**CWE:** CWE-NNN
**Location:** <rel/path>:<line> | <rel/path>:<line_start>-<line_end>
**Sinks:** S1[, S2…]
**Trace:** <markdown>
**Boundary:** <markdown>
**Validation:** <markdown — include the reproduction script verbatim
under a fenced code block. Do NOT execute it; the human will.>
**Prior art:** <markdown — git log / issues / RFC citations>
**Reach:** <markdown — plausible exposed callers>
**Rating:** <markdown — severity + confidence rationale>
**Suggested fix:** <one or two sentences>
## Ruled out
- **S2, S3** (step N) — <one or two sentences>
Use `## Findings\n\n_None._` for a clean report — never omit the section.
Every inventory sink ID must appear in either `Findings → Sinks:` or in
the `Ruled out` list. No preamble, no overall summary, no closing notes.