Skip to main content
Lesson 1 of 2Advanced⏱ 10 mins

CORS and Security Headers

Protecting against browser-based attacks.

0% of path

Learning Objectives

  • Configure actix-cors

##Letting browsers talk to your API

Browsers enforce the same-origin policy: a page loaded from https://app.example.com is not allowed to read responses from https://api.example.com unless the API explicitly says it is okay. That permission system is called CORS (Cross-Origin Resource Sharing).

#When CORS matters

If your frontend (React, Vue, a static site) runs on a different domain or port from your Actix API, the browser will block requests unless your API sends the right CORS headers. Tools like curl and mobile apps are not affected; CORS is a browser rule.

#Preflight requests

For requests that can change data or use custom headers, the browser first sends an OPTIONS request, called a preflight, asking "is this allowed?" Your server answers with headers listing what it permits. Only then does the browser send the real request.

#Adding CORS to Actix Web

code_blockstoml
actix-cors = "0.7"
code_blocksrust
use actix_cors::Cors;
use actix_web::{http, App, HttpServer};

HttpServer::new(|| {
    let cors = Cors::default()
        .allowed_origin("https://app.example.com")
        .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
        .allowed_headers(vec![http::header::AUTHORIZATION, http::header::CONTENT_TYPE])
        .supports_credentials()
        .max_age(3600);

    App::new().wrap(cors)
})
  • allowed_origin lists the exact origins you trust.
  • allowed_methods and allowed_headers control what the browser may send.
  • supports_credentials allows cookies and auth headers, and then you cannot use a wildcard origin.
  • max_age lets the browser cache the preflight answer.

#Do not use the permissive default in production

Cors::permissive() allows every origin, which is handy during development but unsafe for APIs that use cookies or sensitive data. Use an explicit allowlist in production.

#Other important security headers

Headers protect your users beyond CORS. You can add them with middleware::DefaultHeaders:

code_blocksrust
use actix_web::middleware::DefaultHeaders;

App::new().wrap(
    DefaultHeaders::new()
        .add(("X-Content-Type-Options", "nosniff"))
        .add(("X-Frame-Options", "DENY"))
        .add(("Referrer-Policy", "strict-origin-when-cross-origin"))
        .add(("Strict-Transport-Security", "max-age=63072000; includeSubDomains")),
);
  • X-Content-Type-Options: nosniff stops browsers guessing content types.
  • X-Frame-Options: DENY prevents clickjacking.
  • Strict-Transport-Security forces HTTPS on future visits.
  • A Content-Security-Policy header limits where scripts can load from.

#Debugging CORS

Open the browser's developer tools and look at the Network tab. If a request fails with a CORS error, check that the response includes Access-Control-Allow-Origin and that the preflight OPTIONS request returned a successful status.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

Inspect a preflight request.

Starting Code
curl -i -X OPTIONS http://127.0.0.1:8080/ -H "Origin: http://localhost:5173" -H "Access-Control-Request-Method: POST"
Suggested Modification

Run the command and read the response headers.

Expected Result

You see access-control-allow-origin and access-control-allow-methods in the response.

Explanation: These headers are what the browser reads to decide whether the real request may be sent.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Using Cors::permissive() in production

It removes friction during development and gets left in.

Incorrect Pattern
App::new().wrap(Cors::permissive())
Correct Pattern
Cors::default().allowed_origin("https://app.example.com")

Explanation: Allow only the origins you control so other sites cannot read your users' data.

!

Forgetting to allow the Authorization header

Preflight rejects custom headers that are not on the allowlist.

Incorrect Pattern
.allowed_headers(vec![http::header::CONTENT_TYPE])
Correct Pattern
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::CONTENT_TYPE])

Explanation: Add every header your frontend sends, otherwise the browser blocks the call before it reaches your handler.

Practical Exercise

Allow one frontend

Task

Configure CORS so only http://localhost:5173 can call your API with GET and POST.

Requirements

  • Use Cors::default()
  • Allow only that origin
  • Allow GET and POST and the Content-Type header

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Single-page apps and mobile web views hosted on a different domain need exactly this configuration, usually with the allowed origin read from an environment variable per environment.

Key Takeaways

  • CORS is enforced by browsers, not by curl or servers calling each other.
  • Use actix-cors with an explicit allowed_origin list in production.
  • supports_credentials cannot be combined with a wildcard origin.
  • Add security headers with DefaultHeaders (nosniff, frame options, HSTS).

Finished reading?

Mark this lesson as done to track your progress and earn XP.