fix: protect direct auth mutation routes

This commit is contained in:
2026-08-24 14:58:34 +09:00
parent 00a96234c6
commit d1c15ee295
+58 -2
View File
@@ -968,10 +968,10 @@ async fn authorize_workspace_api_request(
mut request: Request, mut request: Request,
next: axum::middleware::Next, next: axum::middleware::Next,
) -> Response { ) -> Response {
if !request.uri().path().starts_with("/api/") || is_server_global_forward(request.uri().path()) if !request.uri().path().starts_with("/api/") {
{
return next.run(request).await; return next.run(request).await;
} }
let public_server_api = is_server_global_forward(request.uri().path());
let workspace_id = api.workspace_id().to_owned(); let workspace_id = api.workspace_id().to_owned();
let proof = request let proof = request
.headers() .headers()
@@ -1020,6 +1020,7 @@ async fn authorize_workspace_api_request(
.await .await
{ {
Ok(Some(actor)) => actor, Ok(Some(actor)) => actor,
Ok(None) if public_server_api => return next.run(request).await,
Ok(None) | Err(_) => return StatusCode::UNAUTHORIZED.into_response(), Ok(None) | Err(_) => return StatusCode::UNAUTHORIZED.into_response(),
}; };
let cookie_authenticated = matches!(actor.auth_method, ActorAuthMethod::BrowserSession); let cookie_authenticated = matches!(actor.auth_method, ActorAuthMethod::BrowserSession);
@@ -15881,6 +15882,61 @@ mod tests {
assert_ne!(csrf_accepted.status(), StatusCode::FORBIDDEN); assert_ne!(csrf_accepted.status(), StatusCode::FORBIDDEN);
} }
#[tokio::test]
async fn direct_workspace_router_enforces_origin_on_browser_auth_mutations() {
let workspace = tempfile::tempdir().unwrap();
let api = test_api(workspace.path()).await;
seed_test_api_token(api.store.as_ref(), "direct-cookie");
api.store
.create_browser_session(&BrowserSessionRecord {
token_hash: crate::auth::token_hash("direct-browser-session"),
session_id: "direct-session".to_owned(),
user_id: "user-direct-cookie".to_owned(),
created_at: "2026-01-01T00:00:00Z".to_owned(),
expires_at: "2099-01-01T00:00:00Z".to_owned(),
revoked_at: None,
})
.unwrap();
let AuthConfig::Passkey {
origin,
cookie_name,
..
} = &api.config.auth;
let origin = origin.clone();
let cookie = format!("{cookie_name}=direct-browser-session");
let app = build_router(api.clone());
let cross_site = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/device-login/approve")
.header(axum::http::header::COOKIE, &cookie)
.header(CONTENT_TYPE, "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(cross_site.status(), StatusCode::FORBIDDEN);
let same_origin = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/device-login/approve")
.header(axum::http::header::COOKIE, cookie)
.header(ORIGIN, origin)
.header(CONTENT_TYPE, "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_ne!(same_origin.status(), StatusCode::FORBIDDEN);
}
#[tokio::test] #[tokio::test]
async fn browser_session_set_and_clear_cookies_follow_public_https_scheme() { async fn browser_session_set_and_clear_cookies_follow_public_https_scheme() {
for (scheme, secure) in [("http", false), ("https", true)] { for (scheme, secure) in [("http", false), ("https", true)] {