A comprehensive guide to enhancing frontend security using Content Security Policy (CSP) and Cross-Origin Resource Sharing (CORS), protecting your web applications from modern threats.
Frontend Security Hardening: Content Security Policy and CORS
In today's interconnected digital landscape, frontend security is paramount. Web applications are increasingly targeted by sophisticated attacks, making robust security measures essential. Two critical components of a secure frontend architecture are Content Security Policy (CSP) and Cross-Origin Resource Sharing (CORS). This comprehensive guide provides an in-depth look at these technologies, offering practical examples and actionable insights to help you fortify your web applications against modern threats.
What is Content Security Policy (CSP)?
Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks. CSP is implemented by the web server sending a Content-Security-Policy HTTP response header to the browser. This header defines a whitelist of sources that the browser is allowed to load resources from. By restricting the sources of content that a browser can load, CSP makes it significantly more difficult for attackers to inject malicious code into your website.
How CSP Works
CSP works by instructing the browser to only load resources (e.g., scripts, stylesheets, images, fonts) from approved sources. These sources are specified in the CSP header using directives. If a browser attempts to load a resource from a source that is not explicitly allowed, it will block the request and report a violation.
CSP Directives: A Comprehensive Overview
CSP directives control the types of resources that can be loaded from specific sources. Here's a breakdown of some of the most important directives:
- default-src: Specifies the default source for all content types. This is a fallback directive that applies when other, more specific directives are not present.
- script-src: Specifies the sources from which scripts can be loaded. This is crucial for preventing XSS attacks.
- style-src: Specifies the sources from which stylesheets can be loaded.
- img-src: Specifies the sources from which images can be loaded.
- font-src: Specifies the sources from which fonts can be loaded.
- media-src: Specifies the sources from which audio and video can be loaded.
- object-src: Specifies the sources from which plugins (e.g., Flash) can be loaded. This is often set to 'none' to disable plugins entirely due to their inherent security risks.
- frame-src: Specifies the sources from which frames (e.g., <iframe>) can be loaded.
- connect-src: Specifies the URLs which the user agent can connect to using script interfaces such as XMLHttpRequest, WebSocket and EventSource.
- base-uri: Specifies the URLs that can be used in a document's <base> element.
- form-action: Specifies the URLs to which form submissions can be sent.
- upgrade-insecure-requests: Instructs the user agent to automatically upgrade insecure requests (HTTP) to secure requests (HTTPS).
- report-uri: Specifies a URL where the browser should send reports about CSP violations. This directive is deprecated in favor of `report-to`.
- report-to: Specifies a reporting group name defined in the `Report-To` header, where the browser should send reports about CSP violations.
CSP Source List Keywords
Within CSP directives, you can use source list keywords to define allowed sources. Here are some common keywords:
- 'self': Allows resources from the same origin (scheme and host) as the document.
- 'none': Disallows resources from all sources.
- 'unsafe-inline': Allows the use of inline scripts and styles (e.g., <script> tags and style attributes). Use with extreme caution as it significantly weakens CSP protection against XSS.
- 'unsafe-eval': Allows the use of dynamic code evaluation functions like
eval()andFunction(). Use with extreme caution as it introduces significant security risks. - 'unsafe-hashes': Allows specific inline event handlers or <style> tags that match a specified hash. Requires browser support. Use with caution.
- 'strict-dynamic': Specifies that the trust explicitly given to a script present in the markup, by accompanying it with a nonce or hash, shall be propagated to all the scripts loaded by that root script.
- data: Allows data: URIs (e.g., inline images encoded as base64). Use with caution.
- https:: Allows resources to be loaded over HTTPS from any domain.
- [hostname]: Allows resources from a specific domain (e.g., example.com). You can also specify a port number (e.g., example.com:8080).
- [scheme]://[hostname]:[port]: A fully qualified URI, allowing resources from the specified scheme, host, and port.
Practical CSP Examples
Let's look at some practical examples of CSP headers:
Example 1: Basic CSP with 'self'
This policy allows resources only from the same origin:
Content-Security-Policy: default-src 'self'
Example 2: Allowing Scripts from a Specific Domain
This policy allows scripts from your own domain and a trusted CDN:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
Example 3: Disabling Inline Scripts and Styles
This policy disallows inline scripts and styles, which is a strong defense against XSS:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'
Important: Disabling inline scripts requires refactoring your HTML to move inline scripts to external files.
Example 4: Using Nonces for Inline Scripts
If you must use inline scripts, use nonces (cryptographically random, single-use tokens) to whitelist specific inline script blocks. This is more secure than 'unsafe-inline'. The server must generate a unique nonce for each request and include it in both the CSP header and the <script> tag.
Content-Security-Policy: default-src 'self'; script-src 'nonce-r4nd0mN0nc3'; style-src 'self'
<script nonce="r4nd0mN0nc3"> console.log('Inline script'); </script>
Note: Remember to generate a new nonce for each request. Don't reuse nonces!
Example 5: Using Hashes for Inline Styles
Similar to nonces, hashes can be used to whitelist specific inline <style> blocks. This is done by generating a SHA256, SHA384, or SHA512 hash of the style content.
Content-Security-Policy: default-src 'self'; style-src 'sha256-HASHEDSTYLES'
<style sha256="HASHEDSTYLES"> body { background-color: #f0f0f0; } </style>
Note: Hashes are less flexible than nonces as any change to the style content will invalidate the hash.
Example 6: Reporting CSP Violations
To monitor CSP violations, use the report-uri or report-to directive:
Content-Security-Policy: default-src 'self'; report-to csp-endpoint;
You'll also need to configure the Report-To header. The Report-To header defines one or more reporting groups, which specify where and how reports should be sent.
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-report"}]}
Testing and Deploying CSP
Implementing CSP requires careful planning and testing. Start with a restrictive policy and gradually loosen it as needed. Use the Content-Security-Policy-Report-Only header to test your policy without blocking resources. This header reports violations without enforcing the policy, allowing you to identify and fix issues before deploying the policy in production.
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint;
Analyze the reports generated by the browser to identify any violations and adjust your policy accordingly. Once you are confident that your policy is working correctly, deploy it using the Content-Security-Policy header.
Best Practices for CSP
- Start with a default-src: Always define a
default-srcto establish a baseline policy. - Be specific: Use specific directives and source list keywords to limit the scope of your policy.
- Avoid 'unsafe-inline' and 'unsafe-eval': These keywords significantly weaken CSP and should be avoided whenever possible.
- Use nonces or hashes for inline scripts and styles: If you must use inline scripts or styles, use nonces or hashes to whitelist specific code blocks.
- Monitor CSP violations: Use the
report-uriorreport-todirective to monitor CSP violations and adjust your policy accordingly. - Test thoroughly: Use the
Content-Security-Policy-Report-Onlyheader to test your policy before deploying it in production. - Iterate and refine: CSP is not a one-time configuration. Continuously monitor and refine your policy to adapt to changes in your application and the threat landscape.
What is Cross-Origin Resource Sharing (CORS)?
Cross-Origin Resource Sharing (CORS) is a mechanism that allows web pages from one origin (domain) to access resources from a different origin. By default, browsers enforce a Same-Origin Policy, which prevents scripts from making requests to a different origin than the one the script originated from. CORS provides a way to selectively relax this restriction, allowing legitimate cross-origin requests while protecting against malicious attacks.
Understanding the Same-Origin Policy
The Same-Origin Policy is a fundamental security mechanism that prevents a malicious script from one website from accessing sensitive data on another website. An origin is defined by the scheme (protocol), host (domain), and port. Two URLs have the same origin if and only if they have the same scheme, host, and port.
For example:
https://www.example.com/app1/index.htmlandhttps://www.example.com/app2/index.htmlhave the same origin.https://www.example.com/index.htmlandhttp://www.example.com/index.htmlhave different origins (different scheme).https://www.example.com/index.htmlandhttps://sub.example.com/index.htmlhave different origins (different host).https://www.example.com:8080/index.htmlandhttps://www.example.com:80/index.htmlhave different origins (different port).
How CORS Works
When a web page makes a cross-origin request, the browser first sends a "preflight" request to the server. The preflight request uses the HTTP OPTIONS method and includes headers that indicate the HTTP method and headers that the actual request will use. The server then responds with headers that indicate whether the cross-origin request is allowed.
If the server allows the request, it includes the Access-Control-Allow-Origin header in the response. This header specifies the origin(s) that are allowed to access the resource. The browser then proceeds with the actual request. If the server does not allow the request, it does not include the Access-Control-Allow-Origin header, and the browser blocks the request.
CORS Headers: A Detailed Look
CORS relies on HTTP headers to communicate between the browser and the server. Here are the key CORS headers:
- Access-Control-Allow-Origin: Specifies the origin(s) that are allowed to access the resource. This header can contain a specific origin (e.g.,
https://www.example.com), a wildcard (*), ornull. Using*allows requests from any origin, which is generally not recommended for security reasons. Using `null` is only appropriate for "opaque responses" such as when the resource is retrieved using the `file://` protocol or a data URI. - Access-Control-Allow-Methods: Specifies the HTTP methods that are allowed for the cross-origin request (e.g.,
GET, POST, PUT, DELETE). - Access-Control-Allow-Headers: Specifies the HTTP headers that are allowed in the cross-origin request. This is important for handling custom headers.
- Access-Control-Allow-Credentials: Indicates whether the browser should include credentials (e.g., cookies, authorization headers) in the cross-origin request. This header must be set to
trueto allow credentials. - Access-Control-Expose-Headers: Specifies which headers can be exposed to the client. By default, only a limited set of headers are exposed.
- Access-Control-Max-Age: Specifies the maximum amount of time (in seconds) that the browser can cache the preflight request.
- Origin: This is a request header sent by the browser to indicate the origin of the request.
- Vary: A general HTTP header, but important for CORS. When `Access-Control-Allow-Origin` is dynamically generated, the `Vary: Origin` header should be included in the response to instruct caching mechanisms that the response varies based on the `Origin` request header.
Practical CORS Examples
Let's look at some practical examples of CORS configurations:
Example 1: Allowing Requests from a Specific Origin
This configuration allows requests only from https://www.example.com:
Access-Control-Allow-Origin: https://www.example.com
Example 2: Allowing Requests from Any Origin (Not Recommended)
This configuration allows requests from any origin. Use with caution as it can introduce security risks:
Access-Control-Allow-Origin: *
Example 3: Allowing Specific Methods and Headers
This configuration allows GET, POST, and PUT methods, and the Content-Type and Authorization headers:
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Example 4: Allowing Credentials
To allow credentials (e.g., cookies), you need to set Access-Control-Allow-Credentials to true and specify a specific origin (you cannot use * when allowing credentials):
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
You also need to set credentials: 'include' in your JavaScript fetch/XMLHttpRequest request.
fetch('https://api.example.com/data', {
credentials: 'include'
})
CORS Preflight Requests
For certain types of cross-origin requests (e.g., requests with custom headers or methods other than GET, HEAD, or POST with Content-Type of application/x-www-form-urlencoded, multipart/form-data, or text/plain), the browser sends a preflight request using the OPTIONS method. The server must respond to the preflight request with the appropriate CORS headers to indicate whether the actual request is allowed.
Here's an example of a preflight request and response:
Preflight Request (OPTIONS):
OPTIONS /data HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Preflight Response (200 OK):
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
The Access-Control-Max-Age header specifies how long the browser can cache the preflight response, reducing the number of preflight requests.
CORS and JSONP
JSON with Padding (JSONP) is an older technique for bypassing the Same-Origin Policy. However, JSONP has significant security risks and should be avoided in favor of CORS. JSONP relies on injecting <script> tags into the page, which can execute arbitrary code. CORS provides a more secure and flexible way to handle cross-origin requests.
Best Practices for CORS
- Avoid using *: Avoid using the wildcard (*) in the
Access-Control-Allow-Originheader, as it allows requests from any origin. Instead, specify the specific origin(s) that are allowed to access the resource. - Be specific with methods and headers: Specify the exact HTTP methods and headers that are allowed in the
Access-Control-Allow-MethodsandAccess-Control-Allow-Headersheaders. - Use Access-Control-Allow-Credentials with caution: Only enable
Access-Control-Allow-Credentialsif you need to allow credentials (e.g., cookies) in cross-origin requests. Be aware of the security implications of allowing credentials. - Secure your preflight requests: Ensure that your server properly handles preflight requests and returns the correct CORS headers.
- Use HTTPS: Always use HTTPS for both the origin and the resources you are accessing cross-origin. This helps protect against man-in-the-middle attacks.
- Vary: Origin: If you are dynamically generating the `Access-Control-Allow-Origin` header, always include the `Vary: Origin` header to prevent caching issues.
CSP and CORS in Practice: A Combined Approach
While CSP and CORS both address security concerns, they operate at different layers and provide complementary protection. CSP focuses on preventing the browser from loading malicious content, while CORS focuses on controlling which origins can access resources on your server.
By combining CSP and CORS, you can create a more robust security posture for your web applications. For example, you can use CSP to restrict the sources from which scripts can be loaded, and CORS to control which origins can access your API endpoints.
Example: Securing an API with CSP and CORS
Let's say you have an API hosted at https://api.example.com that you want to be accessible only from https://www.example.com. You can configure your server to return the following headers:
API Response Headers (https://api.example.com):
Access-Control-Allow-Origin: https://www.example.com
Content-Type: application/json
And you can configure your website (https://www.example.com) to use the following CSP header:
Website CSP Header (https://www.example.com):
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self' https://api.example.com;
This CSP policy allows the website to load scripts and connect to the API, but prevents it from loading scripts or connecting to other domains.
Conclusion
Content Security Policy (CSP) and Cross-Origin Resource Sharing (CORS) are essential tools for hardening the security of your frontend applications. By carefully configuring CSP and CORS, you can significantly reduce the risk of XSS attacks, data injection attacks, and other security vulnerabilities. Remember to start with a restrictive policy, test thoroughly, and continuously monitor and refine your configuration to adapt to changes in your application and the evolving threat landscape. By prioritizing frontend security, you can protect your users and ensure the integrity of your web applications in today's increasingly complex digital world.