Skip to main content

cortex/frontend/
cors.rs

1//! CORS capabilities for the Rocket frontend
2use rocket::fairing::{Fairing, Info, Kind};
3use rocket::http::Header;
4use std::io::Cursor;
5/// Rocket solution for Cross-origin resource sharing
6pub struct CORS();
7
8#[rocket::async_trait]
9impl Fairing for CORS {
10  fn info(&self) -> Info {
11    Info {
12      name: "Add CORS headers to requests",
13      kind: Kind::Response,
14    }
15  }
16
17  async fn on_response<'r>(
18    &self,
19    request: &'r rocket::Request<'_>,
20    response: &mut rocket::Response<'r>,
21  ) {
22    if request.method() == rocket::http::Method::Options
23      || response.content_type() == Some(rocket::http::ContentType::JSON)
24    {
25      // The JSON surface is deliberately public, read-only data; agents authorize with an explicit
26      // `X-Cortex-Token` header (never ambient cookies) and the admin UI is same-origin. So `*` is
27      // the correct origin for public reads — but it MUST NOT be paired with
28      // `Access-Control-Allow-Credentials: true`: that combination is spec-invalid (browsers reject
29      // it) and signals credentialed cross-origin access we never want (a CSRF/data-theft footgun
30      // on any cookie-resolvable `/api/*` route). We omit the credentials header entirely.
31      response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
32      response.set_header(Header::new(
33        "Access-Control-Allow-Methods",
34        "POST, GET, OPTIONS",
35      ));
36      response.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type"));
37      // (No CSP here: the only header this fairing fires on is JSON, which executes no scripts, and
38      // the old `Content-Security-Policy-Report-Only` reported to a `report-uri` route that doesn't
39      // exist — enforcing nothing, collecting nothing. A real *enforcing* CSP belongs on the HTML +
40      // ar5iv-preview surface, which is the open Arm 13 task.)
41    }
42
43    if request.method() == rocket::http::Method::Options {
44      response.set_header(rocket::http::ContentType::Plain);
45      response.set_sized_body(0, Cursor::new(""));
46    }
47  }
48}
49
50#[cfg(test)]
51mod tests {
52  use super::CORS;
53  use rocket::local::blocking::Client;
54  use rocket::serde::json::Json;
55
56  #[rocket::get("/json")]
57  fn json_route() -> Json<&'static str> { Json("ok") }
58
59  fn client() -> Client {
60    let rocket = rocket::build()
61      .mount("/", rocket::routes![json_route])
62      .attach(CORS());
63    Client::tracked(rocket).expect("rocket client")
64  }
65
66  #[test]
67  fn public_json_gets_wildcard_origin_but_no_credentials() {
68    let client = client();
69    let resp = client.get("/json").dispatch();
70    let headers = resp.headers();
71    assert_eq!(headers.get_one("Access-Control-Allow-Origin"), Some("*"));
72    // The spec-invalid / unsafe `* + credentials` combination must never be emitted.
73    assert_eq!(headers.get_one("Access-Control-Allow-Credentials"), None);
74    // CSP is meaningless on a JSON response — no dead report-only header here.
75    assert_eq!(headers.get_one("Content-Security-Policy-Report-Only"), None);
76  }
77}