PHP CORS Handling
Introduction
When developing APIs with PHP, you'll inevitably encounter Cross-Origin Resource Sharing (CORS) issues. CORS is a security feature implemented by browsers to restrict web pages from making requests to a domain different from the one that served the original page. As a PHP developer, understanding and properly handling CORS is essential for creating accessible and secure APIs.
This guide will walk you through everything you need to know about CORS in PHP API development - from basic concepts to practical implementation strategies.
What is CORS?
CORS (Cross-Origin Resource Sharing) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.
Key CORS Headers
Before diving into PHP implementation, let's understand the essential CORS headers:
- Access-Control-Allow-Origin: Specifies which origins can access the resource
- Access-Control-Allow-Methods: Indicates which HTTP methods are allowed
- Access-Control-Allow-Headers: Lists headers that can be used in the request
- Access-Control-Allow-Credentials: Indicates if request can include user credentials
- Access-Control-Max-Age: Specifies how long preflight results can be cached
Basic CORS Implementation in PHP
The simplest way to enable CORS in PHP is by adding the appropriate headers to your API responses:
<?php
// Allow from any origin
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
// Just exit with 200 OK status
exit(0);
}
// Your actual API code here
$data = [
'message' => 'This API supports CORS!',
'status' => 'success'
];
// Return JSON response
header('Content-Type: application/json');
echo json_encode($data);
Output
When accessing this API from a different origin, the browser will receive:
{
"message": "This API supports CORS!",
"status": "success"
}
And the response headers will include the CORS headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Content-Type: application/json
CORS with Specific Origins
In production, allowing requests from any origin (*
) is often too permissive. A more secure approach is to specify which origins are allowed:
<?php
// Define allowed origins
$allowedOrigins = [
'https://example.com',
'https://app.example.com',
'http://localhost:3000'
];
// Get the origin from the request headers
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
// Check if the origin is allowed
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Credentials: true");
}
// Handle preflight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
// Your API logic here
Creating a Reusable CORS Middleware
For larger applications, it's better to create a reusable CORS middleware that can be applied to all your API endpoints:
<?php
class CorsMiddleware {
private $allowedOrigins;
private $allowedMethods;
private $allowedHeaders;
private $allowCredentials;
private $maxAge;
public function __construct(
array $allowedOrigins = ['*'],
array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
array $allowedHeaders = ['Content-Type', 'Authorization'],
bool $allowCredentials = true,
int $maxAge = 86400 // 24 hours
) {
$this->allowedOrigins = $allowedOrigins;
$this->allowedMethods = $allowedMethods;
$this->allowedHeaders = $allowedHeaders;
$this->allowCredentials = $allowCredentials;
$this->maxAge = $maxAge;
}
public function handle() {
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
// Check if origin is allowed
if ($this->allowedOrigins[0] === '*') {
header('Access-Control-Allow-Origin: *');
} elseif (in_array($origin, $this->allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
if ($this->allowCredentials) {
header("Access-Control-Allow-Credentials: true");
}
}
// Set other CORS headers
header('Access-Control-Allow-Methods: ' . implode(', ', $this->allowedMethods));
header('Access-Control-Allow-Headers: ' . implode(', ', $this->allowedHeaders));
header('Access-Control-Max-Age: ' . $this->maxAge);
// Handle preflight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
}
}
// Usage example
$cors = new CorsMiddleware(
['https://example.com', 'http://localhost:3000'],
['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
['Content-Type', 'Authorization', 'X-Requested-With']
);
$cors->handle();
// Your API logic follows...
CORS with PHP Frameworks
Most PHP frameworks provide built-in CORS handling. Let's look at a few examples:
Laravel
Laravel's CORS can be configured using the fruitcake/laravel-cors
package:
// Install via Composer
// composer require fruitcake/laravel-cors
// Configuration in config/cors.php
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://example.com', 'http://localhost:3000'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];
Slim Framework
In Slim, you can add CORS middleware:
use Slim\Factory\AppFactory;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
$app = AppFactory::create();
// CORS middleware
$app->add(function (Request $request, RequestHandler $handler) {
$response = $handler->handle($request);
return $response
->withHeader('Access-Control-Allow-Origin', 'https://example.com')
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
});
// Handle preflight requests
$app->options('/{routes:.+}', function (Request $request, Response $response) {
return $response;
});
// Define your routes here
Common CORS Issues and Solutions
Issue 1: Preflight Requests Not Handled
Problem: The browser sends an OPTIONS request before the actual request, but your server doesn't handle it properly.
Solution: Add specific handling for OPTIONS requests:
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
// Return 200 OK with CORS headers
exit(0);
}
Issue 2: Credentials Not Allowed
Problem: Your API requires cookies or authentication, but CORS blocks credentials.
Solution: Add the appropriate headers and ensure your origin is specific (not wildcard):
header("Access-Control-Allow-Origin: https://example.com"); // Must be specific origin, not *
header("Access-Control-Allow-Credentials: true");
Issue 3: Custom Headers Blocked
Problem: Your requests include custom headers that are being blocked.
Solution: Explicitly allow those headers:
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header");
Testing CORS Configuration
You can verify your CORS implementation using these methods:
- Browser Developer Tools: Check the Network tab for requests and their headers
- curl: Test preflight requests manually:
curl -X OPTIONS -H "Origin: https://example.com" -H "Access-Control-Request-Method: GET" https://api.example.org/endpoint -v
- Online CORS Testers: Use services like
https://cors-test.codehappy.dev/
Security Considerations
While implementing CORS, keep these security points in mind:
- Avoid using wildcards in production: Don't use
Access-Control-Allow-Origin: *
for APIs that handle sensitive data - Validate origins carefully: Only allow trusted domains in your allowed origins list
- Limit exposed headers: Only expose headers that are necessary for clients
- Be careful with credentials: Enable
Access-Control-Allow-Credentials
only when necessary - Consider timeouts: Set appropriate
Access-Control-Max-Age
to balance security and performance
Practical Example: Building a Complete API Endpoint with CORS
Let's tie everything together with a practical example of a RESTful API endpoint that handles CORS correctly:
<?php
// config.php - Configuration settings
$config = [
'cors' => [
'allowed_origins' => ['https://example.com', 'http://localhost:3000'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'allow_credentials' => true,
'max_age' => 86400, // 24 hours
]
];
// cors.php - CORS middleware
function handleCors($config) {
$corsConfig = $config['cors'];
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
if (in_array($origin, $corsConfig['allowed_origins'])) {
header("Access-Control-Allow-Origin: $origin");
if ($corsConfig['allow_credentials']) {
header("Access-Control-Allow-Credentials: true");
}
}
header('Access-Control-Allow-Methods: ' . implode(', ', $corsConfig['allowed_methods']));
header('Access-Control-Allow-Headers: ' . implode(', ', $corsConfig['allowed_headers']));
header('Access-Control-Max-Age: ' . $corsConfig['max_age']);
// Handle preflight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Content-Length: 0');
header('Content-Type: text/plain');
exit(0);
}
}
// api.php - Sample API endpoint
require_once 'config.php';
require_once 'cors.php';
// Handle CORS first
handleCors($config);
// Process the request based on method
switch ($_SERVER['REQUEST_METHOD']) {
case 'GET':
// Example: fetch users
$users = [
['id' => 1, 'name' => 'John Doe'],
['id' => 2, 'name' => 'Jane Smith']
];
$response = ['status' => 'success', 'data' => $users];
break;
case 'POST':
// Example: create a new user
$input = json_decode(file_get_contents('php://input'), true);
$response = ['status' => 'success', 'message' => 'User created', 'data' => $input];
break;
default:
$response = ['status' => 'error', 'message' => 'Unsupported method'];
http_response_code(405);
}
// Send JSON response
header('Content-Type: application/json');
echo json_encode($response);
Client-side Example
Here's how you might call this API from a JavaScript client:
// Regular fetch
fetch('https://api.example.org/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Fetch with credentials
fetch('https://api.example.org/users', {
method: 'POST',
credentials: 'include', // Includes cookies
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({
name: 'New User'
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Summary
Handling CORS in PHP API development is essential for building accessible and secure web services. In this guide, we've covered:
- The fundamentals of CORS and how it works
- Basic implementation of CORS headers in PHP
- Creating reusable CORS middleware
- Framework-specific CORS handling
- Common CORS issues and their solutions
- Testing and security considerations
- A complete practical example
By properly implementing CORS, you ensure that your PHP APIs can be securely accessed by web applications from different origins, while maintaining appropriate security boundaries.
Additional Resources
- MDN Web Docs: CORS
- PHP Manual: Header Function
- CORS on REST API with PHP
- Fruitcake CORS Package for Laravel
Exercises
- Basic CORS Implementation: Create a simple PHP script that returns JSON data and allows CORS from any origin.
- Origin Validation: Modify the script to only allow requests from specific origins of your choice.
- Preflight Handling: Enhance your script to properly handle OPTIONS preflight requests.
- Middleware Creation: Build a reusable CORS middleware class that can be configured with different options.
- Framework Integration: If you're using a PHP framework like Laravel or Slim, implement CORS handling using the framework's recommended approach.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)