Stack logo
Sync up on the latest from Convex.
Tom Redman's avatar
Tom Redman
4 months ago

Understanding CORS: A Developer's Guide

Abstract visual of cross-origin-resource-sharing in the Convex brand colors

Introduction

Cross-Origin Resource Sharing (CORS) is a crucial concept in modern web development, yet it's often misunderstood and can be a source of frustration for many developers. This article aims to demystify CORS, explain its importance, and provide practical guidance on its implementation. Whether you're a seasoned developer or just starting out, this guide will help you navigate the intricacies of CORS and use it effectively in your projects.

1. Life before CORS

The Same Origin Policy

Before CORS, web browsers implemented a strict Same Origin Policy (SOP) to protect users from malicious scripts. This policy restricted web pages from making requests to a different domain than the one serving the web page. For example, if your page was hosted on https://example.com, it couldn't make AJAX requests to https://api.example.com or any other domain. This was a crucial security measure, preventing malicious sites from reading sensitive data from other domains.

Limitations of the pre-CORS web

The SOP, while secure, posed significant limitations for developers building modern web applications. It made it challenging to create apps that needed to communicate with APIs or services on different domains. Developers had to resort to workarounds like JSONP (JSON with Padding) or setting up proxy servers to fetch cross-origin resources. These solutions were either insecure or added unnecessary complexity to applications.

Security implications of cross-origin requests

Allowing unrestricted cross-origin requests would have severe security implications. Without proper controls, a malicious site could make requests to sensitive endpoints on other domains, potentially accessing or modifying private data. For instance, if your banking site allowed unrestricted cross-origin requests, a malicious script on another site could potentially make transactions on your behalf. The challenge was to find a way to enable legitimate cross-origin requests while maintaining security.

2. CORS is your friend

What is CORS?

Cross-Origin Resource Sharing (CORS) is a security 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. CORS works by adding new HTTP headers that let servers describe which origins are permitted to read that information from a web browser. It's not a security feature that blocks access to resources; rather, it's a way to relax the Same Origin Policy in a controlled manner.

How CORS solves cross-origin communication problems

CORS enables servers to specify who can access their resources, giving developers fine-grained control over cross-origin access. When a browser makes a cross-origin request, it adds an Origin header to the request. If the server allows this origin, it responds with an Access-Control-Allow-Origin header, telling the browser it's safe to allow the request. This mechanism allows for secure communication between different domains, enabling scenarios like frontend JavaScript code fetching data from a separate API server.

The role of CORS in modern web development

In the landscape of modern web development, CORS plays a crucial role. It enables microservices architectures where frontend and backend services can be hosted on different domains. For instance, using Convex, you might have your frontend hosted on https://myapp.com while your Convex backend is at https://happy-dog-123.convex.cloud. CORS allows your frontend to securely communicate with your Convex backend:

1import { ConvexProvider, ConvexReactClient } from "convex/react";
2
3const convex = new ConvexReactClient("https://happy-dog-123.convex.cloud");
4
5function App() {
6  return (
7    <ConvexProvider client={convex}>
8      {/* Your app components */}
9    </ConvexProvider>
10  );
11}
12

In this example, CORS enables the secure connection between your frontend and the Convex backend, allowing for seamless data fetching and real-time updates.

3. Myths about CORS

Myth: CORS prevents attackers from accessing resources

A common misconception is that CORS is a security measure that prevents unauthorized access to resources. In reality, CORS is a relaxation of security restrictions, not an additional layer of security. It doesn't stop attackers from making direct requests to your server. Instead, it prevents a malicious website from making requests using the user's credentials. Remember, an attacker can always use tools like cURL to send requests directly to your server. CORS is about protecting the user, not the server.

Myth: CORS is a server-side security feature

While CORS involves server-side configuration, it's primarily enforced by the browser. The server specifies its CORS policy through headers, but it's the browser that enforces these policies. This means that requests from non-browser clients (like cURL or Postman) aren't restricted by CORS. It's crucial to implement proper authentication and authorization on your server independently of CORS.

Myth: CORS is always complex to implement

Many developers view CORS as a complex, hard-to-implement feature. While CORS can indeed become complex in certain scenarios, basic CORS setup is straightforward. For many applications, simply setting the Access-Control-Allow-Origin header to the appropriate value is sufficient. Platforms like Convex handle CORS configuration automatically, further simplifying the process for developers.

4. Implementing CORS on your own

Basic CORS implementation

Implementing basic CORS is as simple as adding the appropriate headers to your server's responses. Here's a basic example using Express.js:

1app.use((req, res, next) => {
2  res.header('Access-Control-Allow-Origin', 'https://your-allowed-origin.com');
3  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
4  next();
5});
6

This middleware allows requests from 'https://your-allowed-origin.com' and specifies which headers are allowed in the request.

If you're using Convex, you can enable CORS like this:

1import { httpAction } from "./_generated/server";
2
3export default httpAction(async (_, request) => {
4  // Handle preflight requests
5  if (request.method === "OPTIONS") {
6    return new Response(null, {
7      status: 204,
8      headers: {
9        "Access-Control-Allow-Origin": "*",
10        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
11        "Access-Control-Allow-Headers": "Content-Type",
12      },
13    });
14  }
15
16  // Handle the actual request
17  if (request.method === "GET" || request.method === "POST") {
18    // Your logic here
19    const responseBody = JSON.stringify({ message: "Hello from Convex!" });
20
21    return new Response(responseBody, {
22      status: 200,
23      headers: {
24        "Content-Type": "application/json",
25        "Access-Control-Allow-Origin": "*",
26      },
27    });
28  }
29
30  // Handle unsupported methods
31  return new Response("Method Not Allowed", { status: 405 });
32});
33

You can read more about this in the Convex docs here.

CORS headers explained

  • Access-Control-Allow-Origin: Specifies which origins can access the resource. Use * to allow any origin, or specify a domain.
  • Access-Control-Allow-Methods: Specifies which HTTP methods are allowed when accessing the resource.
  • Access-Control-Allow-Headers: Indicates which HTTP headers can be used during the actual request.
  • Access-Control-Allow-Credentials: Indicates whether the response can be shared when the credentials flag is true.

Handling preflight requests

For certain types of requests (like PUT or DELETE), browsers send a preflight request using the OPTIONS method. Your server needs to respond to these requests appropriately:

1app.options('*', (req, res) => {
2  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
3  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
4  res.sendStatus(200);
5});
6

Dive deep into preflight requests in this excellent MDN Web Docs: Preflight request.

Edge cases

Using credentials with CORS

This is one edge case that is somewhat a rite of passage for most developers:

When your requests include credentials (like cookies), you need to set Access-Control-Allow-Credentials to true. However, you can't use the wildcard * for Access-Control-Allow-Origin in this case. You must specify the exact origin:

1res.header('Access-Control-Allow-Origin', 'https://your-allowed-origin.com');
2res.header('Access-Control-Allow-Credentials', 'true');
3

Dealing with multiple allowed origins

If you need to allow multiple origins but can't use the wildcard due to credentials, you can check the origin dynamically:

1const allowedOrigins = ['https://app1.com', 'https://app2.com'];
2app.use((req, res, next) => {
3  const origin = req.headers.origin;
4  if (allowedOrigins.includes(origin)) {
5    res.header('Access-Control-Allow-Origin', origin);
6  }
7  next();
8});
9

Common pitfalls and how to avoid them

  • Not handling OPTIONS requests: Ensure your server responds correctly to preflight requests.
  • Mismatched protocols: If your site is HTTPS, ensure CORS is set up for HTTPS as well.
  • Overly permissive CORS: Avoid using * unless absolutely necessary. Be as specific as possible with your allowed origins.
  • Forgetting about subdomains: If you need to allow subdomains, you might need to use a wildcard or check the origin dynamically.

When using Convex, many of these pitfalls are automatically handled for you, allowing you to focus on building your application logic rather than worrying about CORS configuration.

Testing CORS configurations

After implementing CORS, it's crucial to test your configuration thoroughly. Here are some steps to ensure your CORS setup is working correctly:

  1. Use browser developer tools to observe CORS-related headers in network requests.
  2. Test with different origins, including allowed and disallowed ones.
  3. Verify that preflight requests are handled correctly for complex requests.
  4. Check that credentials are handled appropriately if you're using them.
  5. Use online CORS testing tools to simulate various scenarios.

Remember, thorough testing can save you from headaches down the line and ensure your application is secure and functional across different origins.

5. Using a library

While implementing CORS manually is straightforward for simple cases, libraries can simplify the process, especially for complex scenarios. Here are some popular CORS libraries for different platforms:

  1. Express.js: cors middleware

    1const cors = require('cors');
    2app.use(cors());
    3
  2. Python Flask: flask-cors

    1from flask_cors import CORS
    2CORS(app)
    3
  3. ASP.NET Core: Built-in CORS middleware

    1services.AddCors(options =>
    2{
    3    options.AddPolicy("AllowSpecificOrigin",
    4        builder => builder.WithOrigins("http://example.com"));
    5});
    6
  4. Convex: convex-helpers

    1import { corsRouter } from "convex-helpers/server/cors";
    2import { httpRouter } from "convex/server";
    3import { httpAction } from "./_generated/api";
    4
    5const http = httpRouter();
    6const cors = corsRouter(http);
    7cors.route({
    8  path: "/foo",
    9  method: "GET",
    10  handler: httpAction(async () => {
    11    return new Response("ok");
    12  }),
    13});
    14

Pros and cons of using a library vs manual implementation

Pros of using a library:

  • Simplified configuration
  • Handles edge cases and security best practices
  • Often well-tested and community-supported
  • Can save development time

Cons of using a library:

  • May include unnecessary features, increasing bundle size
  • Less control over specific implementation details
  • Potential for over-permissive defaults if not configured correctly

How to choose the right CORS library for your project

When selecting a CORS library:

  1. Consider your platform and existing tech stack
  2. Check for recent updates and active maintenance
  3. Evaluate the level of configuration flexibility
  4. Read documentation and community feedback
  5. Assess the library's approach to security defaults

For many projects, the built-in CORS support in frameworks or platforms (like Convex) is sufficient and well-optimized for the specific use case.

When to choose a library over manual implementation

While manual implementation gives you fine-grained control, using a library can be beneficial in several scenarios:

  1. Complex applications with multiple origins and varying CORS requirements.
  2. When you need to ensure compliance with best practices and security standards.
  3. If you're working with a framework that has a well-maintained CORS library.
  4. When you want to reduce the potential for configuration errors.
  5. If you need advanced features like dynamic origin validation or complex preflight handling.

However, for simple applications or when you need complete control over your CORS implementation, a manual approach might be more appropriate. Always consider your specific use case and requirements when making this decision.

6. Conclusion

Recap of CORS benefits

CORS has revolutionized web development by enabling secure cross-origin communication. It allows for:

  • Separation of frontend and backend services
  • Microservices architectures
  • Third-party API integrations
  • Enhanced user experiences through rich, interconnected web applications

By understanding and properly implementing CORS, developers can build more flexible, scalable, and secure web applications.

Best practices for implementing CORS

  1. Be as specific as possible with allowed origins
  2. Use environment variables for CORS configuration to easily switch between development and production settings
  3. Regularly audit and update your CORS policies
  4. Implement proper authentication and authorization in addition to CORS
  5. Test CORS configuration thoroughly, including edge cases
  6. Keep your CORS implementation up to date with the latest security recommendations

The future of cross-origin resource sharing

As web applications continue to evolve, CORS will inevitably adapt to new challenges and use cases.

As you continue your journey in the beautiful world of #webdev, remember that CORS is not just a hurdle to overcome, but a powerful tool that enables the creation of sophisticated, interconnected web applications.

If you embrace it, understand it, and use it to its full potential in your projects, your applications will remain safe, secure, and predictable.

Further learning

To deepen your understanding of CORS and stay updated with the latest developments, consider exploring these resources:

  1. MDN Web Docs on CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
  2. W3C CORS Specification: https://www.w3.org/TR/cors/
  3. OWASP CORS Guide: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing

Remember, CORS is an evolving standard, so staying informed about updates and best practices is crucial for maintaining secure and efficient web applications.

We encourage you to start implementing CORS in your projects if you haven't already. Begin with simple configurations, test thoroughly, and gradually incorporate more advanced features as needed. With a solid understanding of CORS, you'll be well-equipped to build robust, secure, and interconnected web applications.

Last but not least, I've created a fun & quick video tutorial on exactly how to implement CORS using Convex.

Happy coding!

Build in minutes, scale forever.

Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started