OpenParable
Skip to content
← Back to skills

Symfony Hardening Rule

DevOps & Security
symfony-hardening-rule

Symfony Hardening Rule is an Agent Skill for devops & security published in the symfony/symfony repository on GitHub: a SKILL.md instruction file an AI agent can load to do this task.

Decide whether a recurring hardening invariant is worth a CI gate, and add it without hitting the traps. Covers PHPStan custom rules (implementation-shape checks) and the check-hardening-tests.php convention (test-presence checks). Use when the user says "write a PHPStan rule", "add a hardening rule", "gate this pattern", "catch this in CI", or wants to turn a finding into a durable check.

View on GitHub ↗
QUICK START

How to use this skill

Bring this guide into your coding agent with a prompt tailored to the tool you use.

  1. Open your project in Codex.
  2. Copy and paste the prompt below.
  3. Review files and risks before approving any action.
Prompt to paste
Inspect the Agent Skill at https://github.com/symfony/symfony/blob/HEAD/.agents/skills/symfony-hardening-rule/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/symfony-hardening-rule, but wait for my approval before writing files or running scripts.

Copying this prompt does not install or run the skill. Review third-party files before use.

Symfony Hardening Rule

Turns a recurring hardening invariant into a CI gate. The mechanics are obvious from the three shipped rules (HardenedComparisonRule, UnserializeToStringTrampolineRule, UnserializeMissingAllowedClassesRule): copy the class shape and the registration. This skill is the part the example files cannot teach: when a rule is worth building, and the traps that cost an afternoon.

When to build (and when not)

A PHPStan rule earns its place only if it catches a broad, recurring shape across many sites. The shipped rules each span dozens (every webhook compare; 60+ __unserialize; every unserialize()). If your rule effectively targets one class or one call site, it is too narrow: it carries CI weight for almost no coverage. Use a regression test instead. (A verify-MAC-before-deserialize rule was built and dropped for exactly this reason: it matched a single serializer.)

Pick the mechanism by what the invariant is about:

Invariant is about...Use
A code shape recurring across many sitesa PHPStan rule (this skill)
A test existing for a boundary classextend check-hardening-tests.php
A value being attacker-controlled (data-flow)Psalm taint, or a reviewer note; not a syntactic rule
A curated set being complete (subnets, attributes)a unit test next to the data
One class / one sitea regression test, not a rule

Do not force a taint problem into a syntactic rule: if safe and unsafe look identical at the sink and differ only by where a value came from, the rule is all false positives.

The shape (copy, do not reinvent)

Model the new rule on the existing .github/sa-tools/rules/*Rule.php: global namespace, Symfony license header, implements Rule with getNodeType()/processNode(), a stable identifier('symfony.<camelCase>'), and the narrowest node type that sees the shape.

The traps

  1. Registration is one line plus the file. Drop <Name>Rule.php into .github/sa-tools/rules/ and add its class to the rules: list in .github/sa-tools/rules.neon (which phpstan.dist.neon includes). The folder's autoloader (rules/bootstrap.php, wired once via --autoload-file in static-analysis.yml) and the .gitignore folder whitelist (!rules, !rules/*) pick the file up with no further edits. A rule that needs constructor arguments takes a services: entry tagged phpstan.rules.rule in that same rules.neon instead of the rules: shorthand. (PHPStan only autoloads a project-local rule class early enough via --autoload-file or composer PSR-4 — a neon scanDirectories/bootstrapFiles runs too late — hence the stable bootstrap, which you never touch per rule.)
  2. phpstan analyse counts are nondeterministic (result cache + parallel workers report 0 then N for the same input). Verify the logic with PHPStan\Testing\RuleTestCase in a throwaway composer require phpstan/phpstan phpunit/phpunit project, with fixtures for the unsafe shape (must fire), the fixed shape (silent), and the nearest safe look-alike from the decision boundary (silent). Never gate on a local analyse count.
  3. The diff-lint only flags new errors. phpstan-diff.php compares the PR against the base branch and fails only on errors new to the PR, so standing violations are absorbed. Design target: zero findings on the current tree at introduction (no baseline entry).

Convention path

If the invariant is "this boundary class must ship its regression test", extend check-hardening-tests.php (pure tokenizer, no autoload) instead of writing a rule. Add the boundary predicate and the required-test predicate in its existing style; accepted gaps go in the ALLOWLIST const as a TODO with a reason, never a permanent waiver.

Error handling

  • If the invariant needs data-flow, do not ship a syntactic rule that fakes it. Say it is a taint or checklist item and wait for confirmation on the fallback.
  • Do not edit phpstan-diff.php or the workflow to make a rule pass. Fix the rule.
  • House conventions: Symfony license header, no em-dashes, no Claude/Anthropic credit, comments sparingly, no issue numbers in code.