If you’ve ever built a modern web API, you’ve likely been haunted by the infamous error: "No 'Access-Control-Allow-Origin' header is present on the requested resource." While it feels like a hurdle during development, that error is actually the browser acting as a high-tech bodyguard for your users' data.
The Attack Vector: Cross-Site Data Theft
Imagine you are logged into your online bank at bank.com. Your browser stores a session cookie to keep you logged in. Now, suppose you accidentally visit a malicious site, evil-hacker.net, in another tab.
Without CORS and the Same-Origin Policy (SOP), a script on the hacker's site could trigger a fetch request to bank.com/api/account-details. Since browsers automatically attach cookies to requests for the destination domain, the hacker could theoretically read your balance or execute transactions using your active session.
How CORS Stops the Bleeding
CORS (Cross-Origin Resource Sharing) doesn't necessarily stop a request from leaving the browser, but it strictly dictates who is allowed to read the response. It’s a handshake between the browser and the server.
Simple Requests: The browser sends the request and checks the
Access-Control-Allow-Originheader in the response. If the domain doesn't match, the browser drops the data before the script can touch it.Preflight (OPTIONS): For "risky" actions (like
POSTwith JSON orDELETE), the browser sends a "pre-check" request: "Hey server, is this random site allowed to send you this command?" If the server says no, the actual request is never even sent.
Implementing Modern Protection in Symfony
In the Symfony ecosystem, we don't manually manage these headers in every controller. We use the NelmioCorsBundle. Here is how to configure your application to prevent session hijacking.
1. Installation
Bash
composer require nelmio/cors-bundle
2. Security Configuration (config/packages/nelmio_cors.yaml)
Suppose your frontend lives at https://my-app.com. You want to ensure no other origin can make authenticated calls to your API.
YAML
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['^https://my-app\.com$'] # Only allow your official frontend
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['X-Custom-Auth-Header']
max_age: 3600
paths:
'^/api/':
allow_origin: ['https://my-app.com']
allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
allow_credentials: true # Crucial for session protection!
Why allow_credentials: true is the Key
When you set allow_credentials: true, you tell the browser it’s okay to share cookies and authorization headers. However, for security reasons, when this is active, allow_origin cannot be a wildcard (*).
You must specify the exact domain. This prevents any random site from making "credentialed" calls to your backend using the user's existing session.
A Protected Controller Example
With the bundle configured, your Symfony controllers stay clean. The security logic is handled at the architectural level.
PHP
// src/Controller/AccountController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
class AccountController extends AbstractController
{
#[Route('/api/balance', name: 'api_balance', methods: ['GET'])]
public function getBalance(): JsonResponse
{
// If CORS is misconfigured, a malicious site could
// trigger this, but the browser would prevent the
// malicious script from ever reading this JSON response.
return new JsonResponse([
'balance' => 1250.75,
'currency' => 'USD'
]);
}
}
Final Thoughts
CORS isn't a network firewall; it’s a browser-side security policy. By properly configuring nelmio-cors-bundle in your Symfony project, you aren't just fixing a "bug"—you are ensuring that your users' sessions and sensitive data remain locked within your trusted environment.
Top comments (1)
symfony.com/bundles/NelmioCorsBund...