.agents/skills/symfony-security-triage/SKILL.md
Decides how a finding is handled, not whether the code is wrong. It complements
symfony-security-review (which finds missing hardening) by making the disclosure
call on a report.
The three dispositions and the conventions that record them:
| Disposition | Label | Branch prefix | Process |
|---|---|---|---|
| CVE | Has CVE + severity | cve-* | Private fix, GHSA/CVE, credit, blog post, coordinated release |
| Public hardening | none / Not a security issue | harden-*, hardening-*, pin-*, fix-* | Normal open PR, changelog, no embargo |
| Not a security issue | Not a security issue / Won't fix | n/a or fix-* | Reply to reporter; optionally a doc/robustness PR |
This skill produces a recommendation. The final call belongs to the Symfony security
team; treat its output as a structured argument, and defer to symfony.com/security
for the authoritative "what is not a vulnerability" list.
Whenever this skill says "Wait for confirmation", treat anything other than an explicit affirmative as no: stop and ask the user how they want to proceed.
Before classifying, pin down four things. Guessing any of them produces a wrong call.
template_from_string)?Reproduce if at all possible; an unreproducible report is not yet triable.
Apply in order. The first matching bucket wins.
A genuine improvement where a CVE condition fails. Typical shapes:
__unserialize __toString trampoline guard needs a pre-existing
untrusted unserialize() entry to matter).Severity (match the low/medium/high labels; CVSS is a sanity check, not the goal):
Affected branches: find the oldest version where the vulnerable code exists, intersect
with maintained_versions from https://symfony.com/releases.json. Fix on the lowest
maintained affected branch, then merge up (see the symfony-merge-up skill). Record the
oldest exposure even if it predates maintained versions.
State the recommendation as: disposition + severity + affected maintained branches + the one-line rationale (which decision-tree conditions decided it), then route:
cve-<slug>-<branch>; apply Has CVE + severity; the fix is
prepared privately and goes through the coordinated-disclosure process (request a
GHSA/CVE, credit the reporter, prepare the security release and blog post). Do not
open a public PR or push to a public remote before release. Wait for confirmation
before any outward step.harden-/hardening-/pin-/fix-<slug>; open a
normal PR with a CHANGELOG entry; use symfony-security-review to confirm the fix and
symfony-hardening-rule to add a durable gate where the class recurs.Not a security issue / Won't fix.In every case, the fix follows TDD, component-scoped tests, no em-dashes, no Claude/Anthropic credit, comments sparingly, no issue references in code.
| Finding shape | Disposition | Deciding factor |
|---|---|---|
| SSRF filter (private-network client) bypassed in a default configuration | CVE | default control bypassed, unauthenticated reach |
HTML sanitizer lets a javascript: URL through on a default profile | CVE | sanitizer's core contract bypassed, stored XSS |
| URL generator emits a path that crosses a routing boundary by default | CVE | boundary crossed in default use |
| A signed transport decodes the payload before verifying its MAC | CVE candidate, high | pre-auth RCE if signing is meant to defend a malicious broker; confirm the trust model |
Webhook signature compared with !==, or the secret is ignored | Hardening | opt-in endpoint, user-configured secret, bounded impact |
__unserialize assigns a string property without a \Stringable guard | Hardening | needs a pre-existing untrusted unserialize() entry (game-over precondition) |
| Input-length cap / broader sanitizer coverage added | Hardening | robustness, not a default-exploitable bypass |
| Unbounded recursion / regex backtracking / cache growth on input | Hardening, low | pure DoS, excluded from the CVE pipeline |
| Deserializing bytes through an API documented as trusted-only | Not a security issue | documented contract, caller's responsibility |
| A trusted-channel feature (e.g. ESI/SSI) used to reach internal hosts | Not a security issue | trusted by design |
| Case-insensitive host allowlist with no working bypass | Not a security issue | not a realistic bypass |
cve-* branch, or open a
public PR for a CVE-class finding before the coordinated release. Wait for confirmation.symfony.com/security is the source of truth for what is not a
vulnerability; this skill encodes observed practice, not policy.