how to enable CORS in Flask REST API

Flask CORS Basics: The Header Handshake

Think of CORS not as a "magic switch" you flip, but as a set of HTTP response headers your Flask API must send back to the browser. The browser acts as the security guard. It looks at these headers and decides, "Okay, this response is safe to share with the webpage's JavaScript."

Your job is to ensure Flask includes the right headers. A common pitfall is thinking, "I set the headers, so CORS is enabled." But remember: the browser is the final enforcer. If your Flask response headers don't match what the browser's preflight OPTIONS request asked for, the browser blocks the response—even if your server code is perfect.

Visualizing the CORS Handshake

BROWSER (Frontend)
Waiting for request...
REQ
FLASK SERVER (Backend)
Idle
# Click "Start Request" to see the CORS flow...

As you saw in the simulation, the browser doesn't just ask for data; it asks for permission first. In a REST API, you typically enable CORS in two ways:

  • Public Endpoints: For data like /api/public/data that any website should access. You might allow all origins (*).
  • Private Endpoints: For user-specific data like /api/user/profile. You must restrict origins strictly to your trusted frontend domains. Never use * with credentials (like cookies or Authorization headers).

The Simplest Way: Flask-CORS

While you can manually set headers, it's error-prone. The Flask-CORS extension is the industry standard. It registers a piece of middleware that automatically adds the necessary Access-Control-* headers to every response and handles the OPTIONS preflight requests for you.

# 1. The "Lazy" Way (Good for local dev, risky for production)
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
# This adds Access-Control-Allow-Origin: * to every response
CORS(app)
# 2. The "Intentional" Way (Best for Production)
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
# Restricts access to only your specific frontend
CORS(app, origins=["https://your-frontend.com"])

The key takeaway: Enabling CORS in Flask means configuring your server to send the correct headers in response to both preflight and actual requests. The Flask-CORS extension handles the mechanics; your job is to decide who is allowed to talk to your API.

Flask CORS Overview: The Library Analogy

Imagine your Flask REST API is a public library. Anyone on the internet can request a book (data) from it. But the browser—acting on behalf of a visitor's web page—is the librarian who decides whether to hand that book to the visitor's JavaScript code.

CORS headers are the rules you, as the library owner, write on a sign for the librarian. Without that sign, the librarian (browser) won't give the book to the visitor's script, even if the library (your API) has the book ready.

Visualizing: Global vs. Per-Route CORS

Choose CORS Strategy
# Waiting for traffic...

As you saw in the simulation, Flask, by default, sends no CORS headers. If you create a simple endpoint and call it from a different domain in your frontend JavaScript, the browser will block the response. The "failure" happens entirely in the browser because the required Access-Control-Allow-Origin header is missing.

This is why you must explicitly configure CORS. The Flask-CORS extension is the standard tool—it's not built into Flask core.

Global vs. Per-Route Settings

You generally have two main ways to apply CORS in Flask-CORS. Let's look at the code for both:

# 1. Global: The "Lazy" Way
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
# This adds headers to EVERY route automatically
CORS(app)
# 2. Per-Route: The "Precise" Way
from flask import Flask
from flask_cors import cross_origin

app = Flask(__name__)

# Only this specific route gets the green light
@app.route("/public/data")
@cross_origin()
def public_data():
    return {"data": "public"}

# This route remains closed to outside browsers
@app.route("/private/profile")
def private_profile():
    return {"user": "alice"}

Why Global can be risky: If your API has both public endpoints (like /products) and private ones (like /admin/delete_user), a global setting might accidentally expose your admin routes to any website.

Rule of thumb: Start with per-route CORS for any endpoint that requires authentication or handles sensitive data. Use global only for truly public APIs where every route is safe to expose to any origin.

Why Enable CORS in Flask?

Imagine your Flask API is a speaker at a conference. Your frontend JavaScript code is an audience member in a different room (a different domain). Without CORS, the browser acts like a strict usher.

When your frontend tries to listen to the speaker (fetch data), the usher says, "That speaker isn't authorized to talk to this room," and physically blocks the sound from reaching your ears—even though the speaker is perfectly willing and able to talk. Your frontend code sees a network error, but your Flask server logs show a perfectly successful request.

CORS is you, the conference organizer, giving the speaker (your API) a permission slip to talk to specific rooms (origins). Without that slip, the browser usher will always block the response from your JavaScript.

The Browser Usher Simulation

🛡️ Browser Usher (Same-Origin Policy)
Trusted App
Origin: https://my-app.com
Waiting...
Flask API
Serving Data
200 OK
{ "secret": "data" }
Malicious Site
Origin: https://evil.com
Waiting...
What's happening? The Flask API sends the data to both sites. But the Browser Usher intercepts the response. It sees the "Malicious Site" doesn't have a valid CORS header, so it blocks the data there. The "Trusted App" is allowed to read it.

The Common Pitfall: The "Lazy" Global Allow

The easiest thing to do is add CORS(app) at the top of your Flask file. This is the equivalent of putting a sign on the conference room that says, "Anyone from anywhere can listen to every speaker in this building."

Security Risk: If your API has a route like /api/admin/delete_all_users, and you've globally allowed all origins (*), you've given any malicious website the ability to make a user's browser silently call that admin route (if the user is logged in).

The pitfall isn't using CORS(app)—it's using it without auditing your routes. The rule is: Only enable CORS on endpoints that are genuinely meant to be called from a browser on another domain. Private, authenticated, or admin endpoints should have CORS disabled globally and only enabled explicitly on the specific, public routes that need it.

Benefits for Modern Applications

Single-Page Apps (SPAs)

CORS is what allows your modern SPA—hosted on https://app.example.com—to talk to your cleanly separated Flask REST API on https://api.example.com. Without it, your entire frontend would have to be served from the same domain as your API, making deployment and scaling much harder.

Mobile & Desktop Clients

Native apps (React Native, Electron) are not subject to the browser's same-origin policy. They don't need CORS headers. However, if your API is also consumed by a web version, you still need CORS for the web clients. The headers are simply ignored by native clients.

Performance & Security Trade-offs

  • Performance: The CORS preflight (OPTIONS) request adds a small network round-trip before certain "non-simple" requests. For high-frequency API calls, this adds latency. Mitigation: Keep requests simple or cache preflight responses using Access-Control-Max-Age.
  • Security Reality: Enabling CORS does not authenticate requests. It only tells the browser whether it can share the response with the calling script. A malicious site can still send requests to your API; CORS only controls whether the response can be read. Your API's real security must always come from proper authentication (tokens, sessions) and authorization checks.

The Practical Takeaway: You enable CORS in Flask to unlock browser-based clients for your API. But you must do so selectively. Start by disabling CORS globally and add @cross_origin() only to the specific routes your web frontend needs to call.

Common Misconceptions about CORS in Flask

Now that you know how to enable CORS, let's address the why and the what ifs. Many beginners fall into mental traps that make their APIs either broken or dangerously insecure.

Misconception #1: "CORS is just a browser problem."
This is the most dangerous shortcut. While the browser is the enforcer that blocks the request, your Flask server is the rule-maker. If your server doesn't send the specific permission headers, the browser has no choice but to block the response. You aren't "fixing the browser"; you are teaching your API how to speak to the outside world.

Misconception #2: "I'll just use a wildcard * to allow everything."
It's tempting. CORS(app, origins="*") feels like the "easy button." It works perfectly for public data (like a weather API). But if your app handles logins, cookies, or user data, this is a security sledgehammer that can crack your security wide open.

Simulation: Wildcard vs. Restricted Origins

Flask Server Configuration
# Waiting for attack simulation...
Scenario: A user is logged into your app. They visit a malicious site (evil.com). That site tries to read your API data.

As you saw, the critical difference isn't whether the request reaches your server (it does in both cases). The difference is whether the browser allows the attacker's script to read the response.

Why Wildcards are Dangerous with Credentials:
If you use origins="*" on an endpoint that uses cookies or Auth headers, you are essentially handing a master key to the internet. A malicious site could trick a user's browser into making requests to your API, and because of the wildcard, the browser would happily let that malicious site read the private data back.

The Safe Pattern: Explicit Allowlisting

The industry standard is to be specific. You tell Flask exactly which frontend domains are allowed to talk to your backend. This is called Allowlisting.

# The Secure Configuration
import os
from flask_cors import CORS

# 1. Define allowed origins explicitly (e.g., from env variables)
allowed_origins = [
  "https://my-app.com",
  "https://staging.my-app.com"
]

# 2. Apply CORS with specific origins
CORS(app, origins=allowed_origins, supports_credentials=True)

Why this is safe: When the malicious site (evil.com) tries to read the response, the browser checks the Access-Control-Allow-Origin header. It sees https://my-app.com. Since evil.com does not match, the browser blocks the script from seeing the data. The request still happens (which is why you need server-side auth), but the result is hidden from the attacker.

Professor Pixel's Rule: Never use * if your API uses cookies, Authorization headers, or handles sensitive user data. Always use a list of trusted domains.

How to Enable CORS in Flask

At its core, enabling CORS in Flask is straightforward: it's about attaching specific HTTP headers to your API's responses. Specifically, headers like Access-Control-Allow-Origin.

Your Flask route function doesn't need to change its logic (it still returns JSON or data). It just needs to ensure that when it sends that data back, it carries the "permission slip" (headers) that the browser is looking for.

Common Pitfall: Before writing any code, you must install the library. Flask does not include CORS support by default.

pip install Flask-CORS

You have two main options: Manual Header Injection (hard, error-prone) or the Flask-CORS Extension (easy, standard). Let's compare them.

Implementation Comparison

Backend Code
Server Behavior
Incoming Request
Professor's Note: Notice how the "Manual" way requires you to write extra code just to handle the OPTIONS preflight request. The "Extension" does this automatically.

Approach 1: Manual Header Injection

You can technically add CORS headers yourself using Flask's after_request hook or by manually setting headers on the response object. You should only do this if you are learning how HTTP works or have a very specific, non-standard requirement.

Why it's risky: You must remember to handle the OPTIONS preflight request manually for every route. If you miss one, that specific endpoint will fail in the browser.

# The "Manual" Way (Boilerplate Heavy)
from flask import Flask, jsonify, make_response

app = Flask(__name__)

# 1. Define a helper or hook to add headers
@app.after_request
def add_cors_header(response):
    response.headers["Access-Control-Allow-Origin"] = "https://your-frontend.com"
    return response

# 2. You must also handle OPTIONS manually!
@app.route("/data", methods=["GET", "OPTIONS"])
def get_data():
    if request.method == "OPTIONS":
        return "", 200
    return jsonify({"data": "secret"})

Approach 2: Using Flask-CORS Extension

For 99% of real-world projects, use the Flask-CORS library. It handles the heavy lifting: it automatically adds the headers to your responses and, crucially, it automatically handles the OPTIONS preflight requests for you.

# The "Extension" Way (Clean & Standard)
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
# One line enables it globally or per-route
CORS(app, origins=["https://your-frontend.com"])

@app.route("/data")
def get_data():
    return jsonify({"data": "secret"})
    # No manual headers needed!

Professor Pixel's Rule: Start with the Flask-CORS extension. Only drop down to manual header injection if you have a very specific, advanced need that the library doesn't cover. The extension saves you from subtle bugs and saves you time.

Flask CORS Configuration: The Rulebook Strategy

Think of CORS configuration as writing different rulebooks for different rooms in your API building. You wouldn't give the same access to a public lobby (/api/public/info) as you would to a private server room (/api/admin/settings). Configuration is how you tell Flask-CORS exactly which origins can enter which rooms, and with which methods.

Visualizing: Global vs. Per-Route Configuration

Select Configuration Strategy
# Waiting for configuration...

Your CORS policy isn't a single on/off switch—it's a set of rules that can vary by route. The core question your configuration answers is: "For this particular endpoint, which websites are allowed to read the response, and what kind of requests can they send?"

You configure this by specifying:

  • Origins: Which domains (e.g., https://app.example.com) are trusted.
  • Methods: Which HTTP verbs (GET, POST, etc.) are permitted.
  • Headers: Which request headers (e.g., Authorization) the browser can include.
  • Credentials: Whether cookies or auth headers are allowed (supports_credentials=True).

Common Misconception: "One Size Fits All"

The most frequent mistake is applying the same CORS settings to every endpoint, assuming "if it works for /public/data, it works for /user/profile." This ignores endpoint sensitivity. A public weather data endpoint might safely allow all origins (*), but a user profile endpoint that returns personal data must restrict origins to your trusted frontend domain.

A single global CORS(app, origins="*") line is like posting a sign on your building that says "Everyone, all areas, all the time." It's simple, but it's rarely the right security posture for a real application with mixed public and private endpoints.

Using Decorators for Fine-Grained Control

For APIs with both public and private endpoints, per-route configuration using the @cross_origin() decorator is the safest starting point. This lets you explicitly enable CORS only where needed, keeping it disabled by default on sensitive routes.

# Per-Route Configuration Strategy
from flask import Flask, jsonify
from flask_cors import cross_origin

app = Flask(__name__)

# 1. Public endpoint - any origin can GET this data
@app.route("/api/public/weather")
@cross_origin() # Uses default: allow all origins
def public_weather():
    return jsonify({"temp": 72, "condition": "sunny"})

# 2. Private endpoint - only your frontend can access this
@app.route("/api/user/profile")
@cross_origin(
    origins=["https://app.example.com"],
    supports_credentials=True
)
def user_profile():
    return jsonify({"username": "alice"})

# 3. Admin endpoint - no CORS headers at all
@app.route("/api/admin/dashboard")
def admin_dashboard():
    return jsonify({"stats": "sensitive"}) # No decorator!

Why this works: The decorator applies CORS headers only to the decorated route. Your admin route has no CORS headers, so even if a malicious site somehow triggered a request, the browser would block the response from being read—adding a layer of defense.

Using Config Variables for Environment-Specific CORS

Your allowed origins will differ between development, staging, and production. Hardcoding ["https://app.example.com"] in your source means changing code for every deployment. Instead, drive CORS configuration from environment variables.

# Configuration via Environment Variables
import os
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

# 1. Read allowed origins from environment
# Format: "https://app.example.com,https://staging.app.example.com"
allowed_origins = os.getenv("CORS_ALLOWED_ORIGINS", "").split(",")
if allowed_origins == [""]: allowed_origins = []

# 2. Apply globally but only to the origins list from the environment
# Note: This still affects ALL routes, so use with caution!
CORS(app, origins=allowed_origins, supports_credentials=True)

The critical nuance: Even with environment-driven origins, global application (CORS(app)) still applies to every route. Use this only if every single endpoint in your API is safe to be called from your frontend domains. If you have any non-browser-facing endpoints (webhooks, internal services) or truly public endpoints that should allow all origins, mix approaches:

Professor Pixel's Rule: Start with NO global CORS. Use @cross_origin() decorators for control, and environment variables to keep origin lists out of your codebase. Never assume one configuration fits all your endpoints—match the CORS policy to the intended audience of each route.

Flask CORS Basics: The Preflight Handshake

Think of CORS not as a single header, but as a conversation between your browser and your Flask API. When your frontend JavaScript tries to make a "non-simple" request (like a POST with JSON or a PUT request), the browser acts like a cautious diplomat.

Before sending the real data, the browser sends a preflight OPTIONS request. It's asking: "Hey server, before I send this big PUT request with a custom header, can you confirm it's okay? I want to use method X and header Y. Are you cool with that?"

Your Flask API must answer this OPTIONS question with a clear set of rules in its response headers. Only if the browser likes the answer does it then send the actual request. This entire negotiation is the "Cross-Origin Resource Sharing" handshake.

Visualizing the Preflight Handshake

Configure Scenario
# Waiting for negotiation...

As you saw in the simulation, the most common CORS failure isn't about the Access-Control-Allow-Origin header—it's about a mismatch in the preflight negotiation.

If the browser's preflight OPTIONS request says "I want to use PUT", but your Flask server responds with Access-Control-Allow-Methods: GET, POST, the browser cancels the request immediately. It never even sends the actual PUT.

How Flask Handles OPTIONS Preflight

By default, Flask does nothing special for OPTIONS requests. If you don't handle them, your API returns a 405 Method Not Allowed, and the browser blocks the whole operation. This is why using the Flask-CORS extension is standard practice.

1. The Manual Way (Tedious)

You must explicitly handle OPTIONS for every route.

@app.route("/data", methods=["GET", "POST", "OPTIONS"])
def get_data():
    if request.method == "OPTIONS":
        # You must manually build headers!
        response = make_response()
        response.headers["Access-Control-Allow-Origin"] = "*"
        response.headers["Access-Control-Allow-Methods"] = "GET, POST"
        return response
    return jsonify({"data": "ok"})

2. The Extension Way (Standard)

Flask-CORS intercepts OPTIONS automatically.

from flask_cors import CORS

# One line handles everything
CORS(app, methods=["GET", "POST", "PUT"])

@app.route("/data")
def get_data():
    return jsonify({"data": "ok"})

Best Practice: Always use the Flask-CORS extension. It automatically constructs the correct OPTIONS response based on your configuration. This ensures that if you allow PUT in your config, the browser knows it's allowed during the preflight phase.

Performance Tip: Preflight requests add network latency. You can cache the preflight response using max_age. This tells the browser, "I've already told you what methods are allowed; trust me for the next 6 hours."

# Caching the Preflight Response
from flask_cors import CORS

# Cache preflight for 6 hours (21600 seconds)
CORS(app, max_age=21600)

Flask CORS FAQ: Clearing the Confusion

You've got the basics down, but you likely have questions about the "what ifs" and the "why nots". Let's tackle the most common scenarios I see students face in the lab.

What does "CORS" stand for and why is it needed?

Intuition: CORS is the browser's permission slip for cross-domain data sharing. Your Flask REST API is likely on a different domain (or port) than your frontend JavaScript. Without CORS, the browser acts as a security guard and blocks your frontend code from reading the API's response—even if the API successfully processed the request.

Technical Reason: The browser enforces the same-origin policy by default. CORS is the standardized way for your Flask server to send specific Access-Control-* headers in its responses. These headers tell the browser, "Yes, this other origin is allowed to read this resource."

Why do I get "No 'Access-Control-Allow-Origin' header" errors?

Intuition: The browser is telling you it didn't receive the required "permission slip" header. The request likely reached your server, but your server's response is missing the Access-Control-Allow-Origin header.

Quick Debug Checklist:

  • Did you install Flask-CORS? (pip install Flask-CORS)
  • Did you initialize it? (CORS(app) or @cross_origin())
  • Does your origins list match your frontend's exact URL? (e.g., http://localhost:3000 is different from http://127.0.0.1:3000).

Professor's Lab: The Wildcard Danger

Server Configuration
# Waiting for attack simulation...
Scenario: A user is logged into your app. They visit a malicious site (evil.com). That site tries to read your API data.

Is it safe to use the wildcard * for Access-Control-Allow-Origin?

It is safe only for truly public, unauthenticated endpoints. The wildcard (*) allows any origin to read the response.

Critical Danger: Never use origins="*" on endpoints that use credentials (cookies, Authorization headers). Browsers will not send credentials with a wildcard origin, and it creates a CSRF vulnerability. A malicious site could trick a logged-in user into calling your API.

Rule: Use origins="*" only for endpoints that serve completely public data (e.g., a list of countries). For any endpoint that returns user-specific data, specify exact origins.

How do I enable CORS for a single route instead of the whole app?

Use the @cross_origin() decorator. This is the recommended approach for APIs with mixed public and private endpoints.

# Per-Route Configuration Strategy
from flask_cors import cross_origin

# 1. Public endpoint - any origin can GET this data
@app.route("/api/public/weather")
@cross_origin()
def public_weather():
    return jsonify({"temp": 72})

# 2. Private endpoint - no CORS headers sent
@app.route("/api/admin/secret")
def admin_secret():
    return jsonify({"secret": "data"})

What are the security implications of allowing credentials with CORS?

Allowing credentials (cookies, Authorization headers) means you're saying, "I trust this origin enough to let it act on behalf of the user's authenticated session."

  • Cannot use Wildcards: If you set supports_credentials=True, you cannot use origins="*". You must list specific domains.
  • CSRF Risk: Even with CORS, if your API relies on cookies, a malicious site can trigger actions (like transferring money). The browser sends the cookies automatically. CORS does not prevent this; it only prevents the attacker from reading the response. You need CSRF tokens for protection.

When should I avoid enabling CORS on public APIs?

Avoid enabling CORS when the API is not intended to be called from browser-based JavaScript. CORS is only relevant for browser-enforced same-origin policy.

✅ Enable CORS Web Frontends (React, Vue, Angular) hosted on a different domain.
❌ Disable CORS Mobile Apps (React Native, Swift), CLI tools, Server-to-Server scripts, Webhooks.

How does CORS interact with other security headers like CSP?

CORS and CSP (Content Security Policy) are complementary security layers that solve different problems.

Header Purpose Controls
CORS Cross-origin data access Whether JS on Origin A can read responses from Origin B.
CSP Resource loading Which origins a page can load (scripts, styles, images) from.

Key Point: You can have correct CORS but a broken CSP (or vice versa). Your API might correctly allow your frontend to read responses (CORS is good), but if your frontend page's CSP blocks all external scripts, your JavaScript might not even run to make the fetch. Both headers must be configured correctly.

Post a Comment

Previous Post Next Post