Symfony has comprehensive security features built-in - configure them properly for production deployment on Heroku.
Expedited WAF for Symfony
Expedited WAF adds network level request filtering, preventing malicious attacks from ever touching your application. This enables true defense in depth for your Symfony app—even if your framework has a vulnerability or is improperly configured, Expedited WAF dynamically blocks threats before they reach your code.
From experience, we’ve seen many applications where attacks technically didn’t succeed, but burned through so many server resources that the application went down anyway. By filtering malicious requests at the network edge, Expedited WAF protects both your security and your availability.
New Security Features for Your Symfony App
These capabilities aren’t available out of the box with Symfony, but Expedited WAF adds them instantly:
- Block IP Addresses - Stop malicious actors by IP or CIDR range
- Block User Agents - Filter out bad bots and scrapers
- Block by Geolocation - Restrict traffic by country or region
- DDoS Protection - Stop attacks with CAPTCHA challenges
- Block Anonymous Proxies - Prevent traffic from VPNs and proxy networks
Symfony Security Component
-
Configure security.yaml
# config/packages/security.yaml security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' providers: app_user_provider: entity: class: App\Entity\User property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true provider: app_user_provider form_login: login_path: app_login check_path: app_login logout: path: app_logout access_control: - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/profile, roles: ROLE_USER } -
Use proper password hashing
// src/Controller/RegistrationController.php use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; public function register(UserPasswordHasherInterface $passwordHasher) { $hashedPassword = $passwordHasher->hashPassword( $user, $plainPassword ); $user->setPassword($hashedPassword); } -
Implement password strength requirements
// src/Entity/User.php use Symfony\Component\Validator\Constraints as Assert; class User { #[Assert\NotBlank] #[Assert\Length(min: 12)] #[Assert\Regex( pattern: "/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/", message: "Password must contain uppercase, lowercase, and number" )] private $password; }
Environment & Secrets
-
Use environment variables
# config/packages/doctrine.yaml doctrine: dbal: url: '%env(DATABASE_URL)%' -
Store secrets in Heroku config
heroku config:set APP_SECRET='...' heroku config:set DATABASE_URL='...' -
Set APP_ENV to prod
heroku config:set APP_ENV=prod heroku config:set APP_DEBUG=0 -
Use Symfony secrets management
php bin/console secrets:set API_KEY
CSRF Protection
-
Enable CSRF protection
# config/packages/framework.yaml framework: csrf_protection: ~ -
Use CSRF tokens in forms
{{ form_start(form) }} {{ form_widget(form) }} <button type="submit">Submit</button> {{ form_end(form) }} -
Validate CSRF for AJAX requests
fetch('/api/endpoint', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } });
Input Validation
-
Use Symfony Validator
use Symfony\Component\Validator\Constraints as Assert; class UserDTO { #[Assert\NotBlank] #[Assert\Email] private $email; #[Assert\NotBlank] #[Assert\Length(min: 12)] private $password; } // In controller public function create(ValidatorInterface $validator, UserDTO $dto) { $errors = $validator->validate($dto); if (count($errors) > 0) { return new Response((string) $errors, 400); } } -
Sanitize output in Twig
{# Auto-escaped by default #} {{ user.name }} {# Only use raw when absolutely necessary #} {{ content|raw }}
Database Security (Doctrine)
-
Configure Heroku Postgres
# config/packages/doctrine.yaml doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' server_version: '13' charset: utf8mb4 -
Use parameterized queries
// Good - parameterized $user = $entityManager->getRepository(User::class) ->findOneBy(['email' => $email]); // Bad - SQL injection risk $query = $entityManager->createQuery( "SELECT u FROM App\Entity\User u WHERE u.email = '$email'" ); -
Use DQL or QueryBuilder
$qb = $entityManager->createQueryBuilder(); $qb->select('u') ->from('App\Entity\User', 'u') ->where('u.email = :email') ->setParameter('email', $email); -
Don’t log SQL queries in production
# config/packages/dev/doctrine.yaml only doctrine: dbal: logging: true profiling: true
Security Headers
-
Configure security headers
# config/packages/framework.yaml framework: http_client: default_options: headers: 'X-Frame-Options': 'DENY' 'X-Content-Type-Options': 'nosniff' 'X-XSS-Protection': '1; mode=block'Or use NelmioSecurityBundle:
composer require nelmio/security-bundle# config/packages/nelmio_security.yaml nelmio_security: forced_ssl: enabled: true clickjacking: paths: '^/.*': DENY content_type: nosniff: true xss_protection: enabled: true mode_block: true
Session Security
- Configure secure sessions
# config/packages/framework.yaml framework: session: handler_id: ~ cookie_secure: auto cookie_httponly: true cookie_samesite: lax gc_maxlifetime: 3600
API Security
-
Implement API authentication
# config/packages/security.yaml security: firewalls: api: pattern: ^/api stateless: true jwt: ~ # Using LexikJWTAuthenticationBundle -
Use JWTs for API authentication
composer require lexik/jwt-authentication-bundle# config/packages/lexik_jwt_authentication.yaml lexik_jwt_authentication: secret_key: '%env(resolve:JWT_SECRET_KEY)%' public_key: '%env(resolve:JWT_PUBLIC_KEY)%' pass_phrase: '%env(JWT_PASSPHRASE)%' token_ttl: 3600 -
Implement rate limiting
composer require symfony/rate-limiteruse Symfony\Component\RateLimiter\RateLimiterFactory; #[Route('/api/login', methods: ['POST'])] public function login(RateLimiterFactory $loginLimiter) { $limiter = $loginLimiter->create($request->getClientIp()); if (false === $limiter->consume(1)->isAccepted()) { return new Response('Too many requests', 429); } }
CORS Configuration
-
Configure CORS
composer require nelmio/cors-bundle# config/packages/nelmio_cors.yaml nelmio_cors: defaults: origin_regex: true allow_origin: ['https://yourdomain.com'] allow_methods: ['GET', 'POST'] allow_headers: ['Content-Type', 'Authorization'] max_age: 3600 paths: '^/api/': allow_origin: ['https://yourdomain.com']
File Uploads
-
Validate file uploads
use Symfony\Component\Validator\Constraints as Assert; class UploadDTO { #[Assert\File( maxSize: '5M', mimeTypes: ['image/jpeg', 'image/png'], mimeTypesMessage: 'Please upload a valid image' )] private $file; } -
Store uploads on S3
composer require league/flysystem-aws-s3-v3# config/packages/oneup_flysystem.yaml oneup_flysystem: adapters: s3_adapter: awss3v3: client: aws_s3_client bucket: '%env(S3_BUCKET)%' filesystems: s3_filesystem: adapter: s3_adapter
Error Handling
-
Disable debug mode in production
heroku config:set APP_DEBUG=0 -
Configure custom error pages
# config/packages/framework.yaml framework: error_controller: App\Controller\ErrorController::show -
Don’t expose stack traces
- Set APP_DEBUG=0
- Custom error controller
Logging
-
Configure production logging
# config/packages/prod/monolog.yaml monolog: handlers: main: type: stream path: php://stderr level: error -
Don’t log sensitive data
- Filter passwords, tokens in logs
- Use processors to sanitize
Composer Dependencies
-
Audit dependencies
composer audit composer update -
Keep Symfony updated
composer recipes:update -
Use composer.lock
- Commit to version control
- Ensures consistent builds
Heroku Deployment
-
Create Procfile
web: vendor/bin/heroku-php-apache2 public/ -
Configure buildpack
heroku buildpacks:set heroku/php -
Set environment for production
heroku config:set APP_ENV=prod heroku config:set APP_DEBUG=0 -
Run migrations on deploy
heroku run php bin/console doctrine:migrations:migrate --no-interaction -
Clear cache after deployment
heroku run php bin/console cache:clear
Performance & Caching
-
Enable OpCache
# php.ini opcache.enable=1 opcache.memory_consumption=256 opcache.max_accelerated_files=20000 -
Configure Symfony cache
# config/packages/framework.yaml framework: cache: app: cache.adapter.redis default_redis_provider: '%env(REDIS_URL)%'
Testing
-
Write security tests
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class SecurityTest extends WebTestCase { public function testPageIsSecure() { $client = static::createClient(); $client->request('GET', '/admin'); $this->assertEquals(302, $client->getResponse()->getStatusCode()); } } -
Test on Heroku staging
heroku create myapp-staging git push staging main