140 lines
4.7 KiB
Rust
140 lines
4.7 KiB
Rust
//! The pinned application's default CORS policy: public API origins, no credentials.
|
|
use axum::{
|
|
extract::Request,
|
|
http::{HeaderValue, Method, StatusCode, header},
|
|
middleware::Next,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
pub async fn handle(request: Request, next: Next) -> Response {
|
|
let path = request.uri().path();
|
|
let applies = path.starts_with("/api/") || path == "/sanctum/csrf-cookie";
|
|
if !applies {
|
|
return next.run(request).await;
|
|
}
|
|
let method = request
|
|
.headers()
|
|
.get(header::ACCESS_CONTROL_REQUEST_METHOD)
|
|
.cloned();
|
|
let requested = request
|
|
.headers()
|
|
.get(header::ACCESS_CONTROL_REQUEST_HEADERS)
|
|
.cloned();
|
|
let preflight = request.method() == Method::OPTIONS && method.is_some();
|
|
let mut response = if preflight {
|
|
StatusCode::NO_CONTENT.into_response()
|
|
} else {
|
|
next.run(request).await
|
|
};
|
|
response.headers_mut().insert(
|
|
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
|
HeaderValue::from_static("*"),
|
|
);
|
|
if preflight {
|
|
response.headers_mut().insert(
|
|
header::CONTENT_TYPE,
|
|
HeaderValue::from_static("text/html; charset=UTF-8"),
|
|
);
|
|
response.headers_mut().insert(
|
|
header::ACCESS_CONTROL_ALLOW_METHODS,
|
|
HeaderValue::from_bytes(&method.unwrap().as_bytes().to_ascii_uppercase())
|
|
.expect("validated request header"),
|
|
);
|
|
response.headers_mut().insert(
|
|
header::ACCESS_CONTROL_ALLOW_HEADERS,
|
|
requested.unwrap_or(HeaderValue::from_static("")),
|
|
);
|
|
response.headers_mut().insert(
|
|
header::ACCESS_CONTROL_MAX_AGE,
|
|
HeaderValue::from_static("0"),
|
|
);
|
|
response.headers_mut().append(
|
|
header::VARY,
|
|
HeaderValue::from_static(
|
|
"Access-Control-Request-Method, Access-Control-Request-Headers",
|
|
),
|
|
);
|
|
}
|
|
response
|
|
}
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use axum::{Router, body::Body, http::Request, middleware, routing::get};
|
|
use tower::ServiceExt;
|
|
#[tokio::test]
|
|
async fn preflight_bypasses_handlers_and_never_enables_credentials() {
|
|
let app = Router::new()
|
|
.route("/api/v1/probe", get(|| async { StatusCode::UNAUTHORIZED }))
|
|
.route("/user", get(|| async { StatusCode::UNAUTHORIZED }))
|
|
.layer(middleware::from_fn(handle));
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("OPTIONS")
|
|
.uri("/api/v1/probe")
|
|
.header("origin", "https://example.invalid")
|
|
.header("access-control-request-method", "POST")
|
|
.header(
|
|
"access-control-request-headers",
|
|
"Authorization,Content-Type",
|
|
)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
|
assert_eq!(
|
|
response.headers()["content-type"],
|
|
"text/html; charset=UTF-8"
|
|
);
|
|
assert_eq!(response.headers()["access-control-allow-origin"], "*");
|
|
assert_eq!(response.headers()["access-control-allow-methods"], "POST");
|
|
assert!(
|
|
!response
|
|
.headers()
|
|
.contains_key("access-control-allow-credentials")
|
|
);
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/api/v1/probe")
|
|
.header("origin", "https://example.invalid")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
|
assert_eq!(response.headers()["access-control-allow-origin"], "*");
|
|
let without_origin = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/api/v1/probe")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(without_origin.headers()["access-control-allow-origin"], "*");
|
|
let response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/user")
|
|
.header("origin", "https://example.invalid")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
!response
|
|
.headers()
|
|
.contains_key("access-control-allow-origin")
|
|
);
|
|
}
|
|
}
|