Symfony's Security component is more explicit and more configuration-driven than most PHP frameworks - which is a strength once it's set up correctly, and a source of real gaps when security.yaml is copied from a tutorial and never revisited. This guide covers the Symfony-specific patterns that show up in real applications, alongside the general PHP issues (raw queries, debug mode) that apply here just as they do in Laravel.
1. security.yaml Access Control Is Evaluated Top to Bottom, First Match Wins
access_control rules in security.yaml are checked in order, and the first matching rule applies - not the most specific one. A broad early rule can silently make a more restrictive later rule unreachable.
# security.yaml - the /admin rule below never applies, because
# the / rule above it matches every path first
access_control:
- { path: ^/, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN }
# Fixed: most specific rules first
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: PUBLIC_ACCESS }
2. Voters for Object-Level Authorization
Role checks (ROLE_ADMIN, ROLE_USER) answer "what kind of user is this," not "does this user own this specific object" - the same IDOR gap covered in our IDOR prevention checklist applies directly to Symfony apps that rely on roles alone. Voters are Symfony's mechanism for object-level checks.
// src/Security/Voter/OrderVoter.php
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool {
$user = $token->getUser();
return $subject->getUserId() === $user->getId();
}
// In the controller
$this->denyAccessUnlessGranted('view', $order);
3. Doctrine: Raw DQL and Native Queries Reopen SQL Injection
The Doctrine ORM parameterizes queries built through its QueryBuilder and repository methods automatically. String-concatenated DQL or native SQL bypasses that protection exactly the same way Laravel's whereRaw does.
// Vulnerable
$dql = "SELECT o FROM App\Entity\Order o WHERE o.status = '" . $status . "'";
// Fixed: bound parameters, even in raw DQL/native queries
$query = $em->createQuery('SELECT o FROM App\Entity\Order o WHERE o.status = :status');
$query->setParameter('status', $status);
4. Twig Auto-Escapes by Default - the |raw Filter Turns It Off
Twig escapes output automatically, the same protection React and Vue provide by default. The |raw filter is Twig's equivalent of dangerouslySetInnerHTML - it exists for genuine cases (rendering trusted, pre-sanitized HTML) but disables escaping entirely for anything piped through it.
{# Vulnerable if $comment came from user input #}
{{ comment.body|raw }}
{# Safe: auto-escaped by default #}
{{ comment.body }}
Grepping a Symfony codebase for |raw is as high-signal a check as grepping a React codebase for dangerouslySetInnerHTML - see our DOM-based XSS guide for the equivalent pattern across frameworks.
5. APP_ENV and APP_DEBUG in Production
Symfony's debug mode (APP_ENV=dev or APP_DEBUG=1) exposes the full exception page, container configuration, and route map through the web-based profiler. This is the identical class of exposure covered for Laravel's APP_DEBUG in our Laravel security guide - confirm it against the live, deployed site, not just the .env file, since a staging config sometimes leaks into production through a copy-paste deploy script.
# .env.local (production) - never committed
APP_ENV=prod
APP_DEBUG=0
6. Serializer Context Groups Prevent Mass Assignment and Over-Exposure
Symfony's Serializer component, used heavily in API Platform and custom API controllers, will serialize/deserialize every property on an entity unless normalization/denormalization context groups constrain it - the same allowlist principle covered in our Laravel API security guide, applied to Symfony's own serialization layer.
#[Groups(['user:read'])]
private string $email;
#[Groups(['user:write'])] // separate group - not exposed on read
private string $password;
7. Secrets: Symfony Vault, Not Committed .env Files
Symfony's secrets vault (bin/console secrets:set) encrypts sensitive values at rest and keeps the decryption key out of version control - a real improvement over a plaintext .env file, but only if it's actually used instead of a convenient .env.local that ends up committed anyway during a rushed deploy.
Checking Your Symfony App From the Outside
None of the above is visible from reading your codebase's public GitHub page - only from what the live application actually serves. Shieldome checks security headers, debug mode exposure, and exposed configuration files regardless of backend framework. Create a free account to run your first scan.