Introduction to Browser Security Models
Web browsers implement several security mechanisms to protect users as they browse the internet. Today, we'll focus on two critical browser security features that you need to understand as full-stack developers:
- CORS (Cross-Origin Resource Sharing) - Controls which sites can access your API
- CSP (Content Security Policy) - Controls what resources can be loaded by your site
Both of these mechanisms build upon a fundamental browser security concept: the Same-Origin Policy.
Understanding the Same-Origin Policy
The Same-Origin Policy (SOP) is a critical security mechanism implemented by web browsers that restricts how a document or script loaded from one origin can interact with resources from another origin.
An origin is defined by the combination of:
- Protocol (http, https)
- Host (domain name)
- Port (80, 443, etc.)
Origin Examples
| URL | Origin |
|---|---|
| https://example.com/page.html | https://example.com |
| https://example.com:443/page.html | https://example.com |
| http://example.com/page.html | http://example.com |
| https://subdomain.example.com/page.html | https://subdomain.example.com |
| https://example.com:8080/page.html | https://example.com:8080 |
What the Same-Origin Policy Restricts
Under the Same-Origin Policy, a web page can only make direct requests to resources from the same origin. This prevents malicious websites from:
- Reading sensitive data from another site's cookies or localStorage
- Accessing DOM elements from another site
- Making AJAX requests to APIs on different origins
What is Allowed Across Origins
The Same-Origin Policy does allow some cross-origin interactions:
- Loading images (<img> tags)
- Loading CSS (<link> tags)
- Loading scripts (<script> tags)
- Loading iframes (<iframe> tags), though the contained page is isolated
- Form submissions (but JavaScript can't see the response)
Real-World Analogy
Think of the Same-Origin Policy like a building's security system. People from Company A can freely interact with Company A's offices, use the meeting rooms, and access documents. But they cannot enter Company B's offices in the same building without explicit permission, even though both companies share the building (the web browser).
Cross-Origin Resource Sharing (CORS)
While the Same-Origin Policy provides essential security, modern web applications often need to make requests across different origins. CORS is a mechanism that allows a server to selectively relax the Same-Origin Policy, enabling cross-origin requests in a controlled manner.
How CORS Works
When a website makes a cross-origin request, the browser automatically adds an Origin header to the request. The server checks this header and decides whether to allow the request by sending appropriate CORS headers in the response.
Simple CORS Request & Response
// Browser automatically adds:
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
// Server responds with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"data": "This is accessible cross-origin"}
CORS Headers
The main CORS response headers are:
- Access-Control-Allow-Origin: Specifies which origins can access the resource
- Access-Control-Allow-Methods: Specifies allowed HTTP methods (GET, POST, etc.)
- Access-Control-Allow-Headers: Specifies which headers can be used in the request
- Access-Control-Allow-Credentials: Indicates whether the request can include credentials (cookies)
- Access-Control-Expose-Headers: Specifies which response headers are accessible to JavaScript
- Access-Control-Max-Age: Specifies how long preflight results can be cached
Preflight Requests
For "non-simple" requests (like those using PUT/DELETE methods, custom headers, or content types other than application/x-www-form-urlencoded, multipart/form-data, or text/plain), browsers send a preflight request using the OPTIONS method to check if the actual request is allowed.
Preflight Request & Response
// Preflight request (automatic by browser)
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
// Preflight response from server
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Only after this preflight check passes will the actual PUT request be sent.
Implementing CORS in Express.js
In Express.js applications, the cors middleware makes it easy to implement CORS:
Basic CORS Implementation
const express = require('express');
const cors = require('cors');
const app = express();
// Allow all origins (not recommended for production)
app.use(cors());
// OR with specific options
app.use(cors({
origin: 'https://app.example.com', // Allow only this origin
methods: ['GET', 'POST'], // Allow only GET and POST
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // Allow cookies
}));
// Route-specific CORS
app.get('/api/public-data', cors(), (req, res) => {
res.json({ data: 'This API is accessible cross-origin' });
});
// Restricted API with custom CORS configuration
const restrictedApiCors = cors({
origin: ['https://admin.example.com', 'https://internal.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true
});
app.put('/api/protected-data', restrictedApiCors, (req, res) => {
// Only allowed origins can access this endpoint
res.json({ data: 'Protected data updated' });
});
Handling Dynamic Origins
For more complex scenarios, you can use a function for the origin option:
app.use(cors({
origin: function(origin, callback) {
// Allow requests with no origin (like mobile apps, curl)
if (!origin) return callback(null, true);
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
/\.example\.net$/ // Allow all subdomains of example.net
];
// Check if origin is allowed
let allowed = false;
for (let allowedOrigin of allowedOrigins) {
if (typeof allowedOrigin === 'string' && origin === allowedOrigin) {
allowed = true;
break;
} else if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {
allowed = true;
break;
}
}
if (allowed) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Real-World CORS Scenario: Third-Party API Integration
Imagine you're building a weather dashboard app at weather-app.com that integrates with a third-party weather API at weather-api.com. Without CORS, your frontend JavaScript couldn't directly fetch data from the weather API. You'd need to proxy all requests through your own backend server. With CORS properly configured on the weather API, your frontend can make direct requests, reducing latency and simplifying your architecture.
Content Security Policy (CSP)
Content Security Policy is a browser security feature that helps prevent cross-site scripting (XSS), clickjacking, and other code injection attacks by controlling which resources can be loaded and executed on your web page.
How CSP Works
CSP works by declaring approved sources of content that the browser can load. This is done through the Content-Security-Policy HTTP header or a <meta> tag in the HTML.
Setting CSP via HTTP Header
// Express.js example
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://trusted-cdn.com; " +
"style-src 'self' https://trusted-styles.com; " +
"img-src 'self' https://trusted-images.com data:; " +
"font-src 'self' https://trusted-fonts.com; " +
"connect-src 'self' https://api.example.com;"
);
next();
});
Setting CSP via Meta Tag
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://trusted-cdn.com">
CSP Directives
CSP provides several directives to control different types of resources:
- default-src: Fallback for other resource types if not specified
- script-src: Valid sources for JavaScript
- style-src: Valid sources for stylesheets
- img-src: Valid sources for images
- connect-src: Valid targets for fetch, XHR, WebSocket, and EventSource
- font-src: Valid sources for fonts
- media-src: Valid sources for audio and video
- frame-src: Valid sources for iframes
- object-src: Valid sources for <object>, <embed>, and <applet>
- base-uri: Restricts URLs for <base> elements
- form-action: Valid targets for form submissions
- frame-ancestors: Valid parents for embedding (protects against clickjacking)
Source Values
CSP directives accept various source values:
- 'self': The same origin as the page
- 'none': No sources are allowed
- 'unsafe-inline': Allows inline scripts and styles (avoid if possible)
- 'unsafe-eval': Allows use of eval() and similar (avoid if possible)
- https://example.com: A specific domain
- https://*.example.com: All subdomains of example.com
- https://: Any domain using HTTPS
- data:: Allows data: URIs (commonly used for images)
- nonce-{random}: Allows scripts with a matching nonce attribute
- sha256-{hash}: Allows scripts with a matching hash
Using Nonces for Inline Scripts
Nonces provide a way to allow specific inline scripts while maintaining strong security:
// Server-side: Generate a random nonce for each request
const crypto = require('crypto');
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'`);
next();
});
// In your template engine (e.g., EJS)
<script nonce="<%= cspNonce %>">
// This inline script is allowed because it has the correct nonce
console.log('CSP with nonce allows this script!');
</script>
CSP Reporting
CSP can also be configured to report violations without blocking content, which is useful during development and testing:
Setting Up CSP Reporting
// Report-only mode (doesn't block anything, just reports violations)
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy-Report-Only',
"default-src 'self'; " +
"report-uri /csp-violation-report");
next();
});
// Endpoint to receive CSP violation reports
app.post('/csp-violation-report', (req, res) => {
console.log('CSP Violation:', req.body);
res.status(204).end();
});
Real-World CSP Example: Google
Google uses a robust CSP on many of its services. For example, on Google Search, they use nonces for inline scripts, restrict connections to their own domains, and have a detailed reporting system for violations. This helps protect millions of users from potential XSS attacks, even if a vulnerability were to be found in their application code.
Implementing CORS and CSP Together
In a full-stack application, you'll typically implement both CORS on your backend API and CSP on your frontend application. Here's how they work together:
Express Backend with CORS and Helmet (for CSP)
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const app = express();
// CORS configuration
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
// Basic security headers including CSP
app.use(helmet());
// Custom CSP configuration
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://trusted-cdn.com'],
styleSrc: ["'self'", 'https://trusted-styles.com'],
imgSrc: ["'self'", 'data:', 'https://trusted-images.com'],
connectSrc: ["'self'", 'https://api.example.com'],
fontSrc: ["'self'", 'https://trusted-fonts.com'],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
frameAncestors: ["'self'"]
}
}));
// API routes
app.get('/api/data', (req, res) => {
res.json({ message: 'This API is secured with CORS and CSP' });
});
// Static files
app.use(express.static('public'));
app.listen(3000, () => {
console.log('Server running on port 3000');
});
React Frontend with CSP Considerations
When building frontend applications, especially with frameworks like React, you need to be careful with CSP implementation:
CSP Challenges with React
-
Inline styles: React often uses inline styles that may be blocked by CSP
// This might be blocked by CSP <div style={{ color: 'red' }}>Dynamic style</div> -
eval() in development: Some bundlers use eval() for hot module replacement
// CSP with webpack dev server needs: contentSecurityPolicy: { directives: { scriptSrc: ["'self'", "'unsafe-eval'"] // Only in development } }
Solutions for React Apps
// Using style-loader in webpack config
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
// Using styled-components for CSS-in-JS
import styled from 'styled-components';
const RedDiv = styled.div`
color: red;
`;
function App() {
return <RedDiv>This works with strict CSP</RedDiv>;
}
Common CORS and CSP Issues
As you implement these security features, you'll likely encounter some common problems:
CORS Troubleshooting
Problem: Credentials Not Included
// Frontend Error:
// "Cross-Origin Request Blocked: The Same Origin Policy disallows
// reading the remote resource at https://api.example.com/data.
// (Reason: CORS header 'Access-Control-Allow-Origin' does not
// match 'https://app.example.com')."
// Solution:
// Server needs to specify the exact origin:
res.header('Access-Control-Allow-Origin', 'https://app.example.com');
res.header('Access-Control-Allow-Credentials', 'true');
// And the client must include credentials:
fetch('https://api.example.com/data', {
credentials: 'include' // Send cookies
});
Problem: Preflight Fails
// Options request gets 404 because the route isn't handling OPTIONS method
// Solution:
app.options('/api/data', cors()); // Respond to preflight for this route
// Or globally:
app.options('*', cors()); // Handle OPTIONS preflight for all routes
CSP Troubleshooting
Problem: Blocked Inline Scripts
// Console Error:
// "Refused to execute inline script because it violates the
// following Content Security Policy directive: "script-src 'self'"."
// Solution:
// Use nonces for necessary inline scripts:
const nonce = crypto.randomBytes(16).toString('base64');
// In your CSP header:
`script-src 'self' 'nonce-${nonce}'`
// In your HTML:
<script nonce="${nonce}">
// Your inline code here
</script>
Problem: Third-Party Resources Blocked
// Console Error:
// "Refused to load the image 'https://external-site.com/image.jpg'
// because it violates the following CSP directive: "img-src 'self'"."
// Solution:
// Expand your CSP to include necessary resources:
`img-src 'self' https://external-site.com`
Debugging Tip: The Network Tab
When facing CORS or CSP issues, the browser's Developer Tools Network tab is your best friend. For CORS issues, look for the preflight OPTIONS request and check response headers. For CSP issues, the Console tab will show specific CSP violations with details about which directive was violated.
Best Practices
CORS Best Practices
- Be specific with origins: Don't use
Access-Control-Allow-Origin: *for APIs that handle sensitive data - Limit allowed methods and headers: Only allow what your application needs
- Use credentials carefully: Only enable if you need cookies or auth headers
- Set appropriate Max-Age: Reduce preflight requests but balance with ability to update policies
- Don't implement CORS yourself: Use well-tested middleware like
corsfor Express - Consider environment-specific configurations: More permissive in development, stricter in production
CSP Best Practices
- Start with Report-Only mode: Use
Content-Security-Policy-Report-Onlyto identify issues before enforcing - Avoid 'unsafe-inline' and 'unsafe-eval': Use nonces or hashes instead
- Set object-src to 'none': Unless you specifically need Flash or Java applets
- Use frame-ancestors to prevent clickjacking: More effective than X-Frame-Options
- Include a fallback policy: Set
default-src 'self'as a baseline - Add upgrade-insecure-requests: Automatically upgrade HTTP requests to HTTPS
- Use report-uri: Collect violation reports to identify issues
Complete Security Headers Setup
// Using Helmet in Express for comprehensive header security
const helmet = require('helmet');
const app = express();
app.use(helmet()); // Adds basic security headers
// Custom CSP configuration
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
imgSrc: ["'self'", 'data:', 'https://trusted-images.com'],
connectSrc: ["'self'", 'https://api.example.com'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'self'"],
frameAncestors: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
reportUri: '/csp-report'
}
})
);
// Generate nonce for each request
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
Practical Exercises
Exercise 1: CORS Configuration
Implement CORS in an Express.js application with the following requirements:
- Allow requests from
https://app.example.comandhttps://admin.example.com - Allow GET, POST, and PUT methods
- Allow Content-Type and Authorization headers
- Allow credentials
- Set preflight cache to 1 hour
Exercise 2: Testing CORS
Create a simple HTML page that makes fetch requests to your API. Test the following scenarios:
- Simple GET request
- Request with custom headers that triggers a preflight
- Request from an unauthorized origin
Exercise 3: Implementing CSP
Implement a Content Security Policy for a web application with the following requirements:
- Allow scripts only from your own domain and a CDN
- Allow styles only from your own domain
- Allow images from your own domain and data: URIs
- Allow fonts from Google Fonts
- Block all plugins (object-src)
- Prevent iframe embedding except from your own domain
- Set up CSP reporting to a local endpoint
Exercise 4: CSP Analysis
Use the browser's Developer Tools to analyze the CSP of three popular websites:
- Identify which directives they use
- Note any use of 'unsafe-inline' or 'unsafe-eval'
- Check if they implement report-uri
- Compare the strictness of their policies
Additional Resources
Next Class Preparation
For our next session on Rate Limiting and DDOS Protection, please familiarize yourself with:
- Basic concepts of rate limiting
- Different types of DDOS attacks
- Explore the
express-rate-limitnpm package