judu/ex-egopublic Fork
⑂ 804247e
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

fix(auth): cookies SameSite=Lax (PWA Android) + en-têtes anti-iframe/cross-siteUnverified

Julien Durillon committed 2026-08-07T20:09:29+02:00 Browse files
804247e parent: 254a88c
modified src/auth/extractor.rs +1 -1
@@ -35,7 +35,7 @@ pub struct AuthRedirect;
3535
3636 impl IntoResponse for AuthRedirect {
3737 fn into_response(self) -> Response {
38- let clear = format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict");
38+ let clear = format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax");
3939 ([(SET_COOKIE, clear)], Redirect::to("/login")).into_response()
4040 }
4141 }
@@ -35,7 +35,7 @@ pub struct AuthRedirect;
35 35
36 impl IntoResponse for AuthRedirect {36 impl IntoResponse for AuthRedirect {
37 fn into_response(self) -> Response {37 fn into_response(self) -> Response {
38- let clear = format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict");38+ let clear = format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax");
39 ([(SET_COOKIE, clear)], Redirect::to("/login")).into_response()39 ([(SET_COOKIE, clear)], Redirect::to("/login")).into_response()
40 }40 }
41 }41 }
modified src/routes/login.rs +4 -1
@@ -61,7 +61,10 @@ pub async fn login_submit(
6161 .path("/")
6262 .http_only(true)
6363 .secure(true)
64- .same_site(SameSite::Strict)
64+ // Lax, pas Strict : au lancement d'une PWA Android (intent du lanceur),
65+ // Chrome retient les cookies Strict → re-login à chaque ouverture.
66+ // Les POST restent protégés par le token CSRF.
67+ .same_site(SameSite::Lax)
6568 .max_age(time::Duration::seconds(SESSION_SECONDS));
6669 Ok((jar.add(cookie), Redirect::to("/")).into_response())
6770 }
@@ -61,7 +61,10 @@ pub async fn login_submit(
61 .path("/")61 .path("/")
62 .http_only(true)62 .http_only(true)
63 .secure(true)63 .secure(true)
64- .same_site(SameSite::Strict)64+ // Lax, pas Strict : au lancement d'une PWA Android (intent du lanceur),
65+ // Chrome retient les cookies Strict → re-login à chaque ouverture.
66+ // Les POST restent protégés par le token CSRF.
67+ .same_site(SameSite::Lax)
65 .max_age(time::Duration::seconds(SESSION_SECONDS));68 .max_age(time::Duration::seconds(SESSION_SECONDS));
66 Ok((jar.add(cookie), Redirect::to("/")).into_response())69 Ok((jar.add(cookie), Redirect::to("/")).into_response())
67 }70 }
modified src/routes/mod.rs +29 -1
@@ -6,7 +6,10 @@ mod stats;
66 mod teams;
77
88 use axum::Router;
9-use axum::response::Redirect;
9+use axum::extract::Request;
10+use axum::http::HeaderValue;
11+use axum::middleware::{self, Next};
12+use axum::response::{Redirect, Response};
1013 use axum::routing::{get, post};
1114
1215 use crate::auth::extractor::AuthUser;
@@ -53,9 +56,34 @@ pub fn router(state: AppState) -> Router {
5356 .route("/manifest.json", get(statics::manifest))
5457 .route("/icon-512.png", get(statics::icon))
5558 .fallback(fallback_404)
59+ .layer(middleware::from_fn(security_headers))
5660 .with_state(state)
5761 }
5862
63+/// En-têtes posés sur toutes les réponses : interdiction d'embarquer l'app
64+/// dans une iframe et de lire ses ressources depuis un autre site.
65+async fn security_headers(req: Request, next: Next) -> Response {
66+ let mut res = next.run(req).await;
67+ let headers = res.headers_mut();
68+ headers.insert("x-frame-options", HeaderValue::from_static("DENY"));
69+ // frame-ancestors seulement : une CSP complète casserait le <script>
70+ // inline d'add_step2 et les styles inline.
71+ headers.insert(
72+ "content-security-policy",
73+ HeaderValue::from_static("frame-ancestors 'none'"),
74+ );
75+ headers.insert(
76+ "cross-origin-opener-policy",
77+ HeaderValue::from_static("same-origin"),
78+ );
79+ headers.insert(
80+ "cross-origin-resource-policy",
81+ HeaderValue::from_static("same-origin"),
82+ );
83+ headers.insert("x-content-type-options", HeaderValue::from_static("nosniff"));
84+ res
85+}
86+
5987 /// L'app s'ouvre sur l'écran d'ajout (comme la maquette).
6088 async fn root(_auth: AuthUser) -> Redirect {
6189 Redirect::to("/temps/nouveau")
@@ -6,7 +6,10 @@ mod stats;
6 mod teams;6 mod teams;
7 7
8 use axum::Router;8 use axum::Router;
9-use axum::response::Redirect;9+use axum::extract::Request;
10+use axum::http::HeaderValue;
11+use axum::middleware::{self, Next};
12+use axum::response::{Redirect, Response};
10 use axum::routing::{get, post};13 use axum::routing::{get, post};
11 14
12 use crate::auth::extractor::AuthUser;15 use crate::auth::extractor::AuthUser;
@@ -53,9 +56,34 @@ pub fn router(state: AppState) -> Router {
53 .route("/manifest.json", get(statics::manifest))56 .route("/manifest.json", get(statics::manifest))
54 .route("/icon-512.png", get(statics::icon))57 .route("/icon-512.png", get(statics::icon))
55 .fallback(fallback_404)58 .fallback(fallback_404)
59+ .layer(middleware::from_fn(security_headers))
56 .with_state(state)60 .with_state(state)
57 }61 }
58 62
63+/// En-têtes posés sur toutes les réponses : interdiction d'embarquer l'app
64+/// dans une iframe et de lire ses ressources depuis un autre site.
65+async fn security_headers(req: Request, next: Next) -> Response {
66+ let mut res = next.run(req).await;
67+ let headers = res.headers_mut();
68+ headers.insert("x-frame-options", HeaderValue::from_static("DENY"));
69+ // frame-ancestors seulement : une CSP complète casserait le <script>
70+ // inline d'add_step2 et les styles inline.
71+ headers.insert(
72+ "content-security-policy",
73+ HeaderValue::from_static("frame-ancestors 'none'"),
74+ );
75+ headers.insert(
76+ "cross-origin-opener-policy",
77+ HeaderValue::from_static("same-origin"),
78+ );
79+ headers.insert(
80+ "cross-origin-resource-policy",
81+ HeaderValue::from_static("same-origin"),
82+ );
83+ headers.insert("x-content-type-options", HeaderValue::from_static("nosniff"));
84+ res
85+}
86+
59 /// L'app s'ouvre sur l'écran d'ajout (comme la maquette).87 /// L'app s'ouvre sur l'écran d'ajout (comme la maquette).
60 async fn root(_auth: AuthUser) -> Redirect {88 async fn root(_auth: AuthUser) -> Redirect {
61 Redirect::to("/temps/nouveau")89 Redirect::to("/temps/nouveau")
modified src/team.rs +2 -1
@@ -21,7 +21,8 @@ pub fn team_cookie(team_id: Uuid) -> Cookie<'static> {
2121 .path("/")
2222 .http_only(true)
2323 .secure(true)
24- .same_site(SameSite::Strict)
24+ // Lax comme tt_session : envoyé au lancement d'une PWA Android.
25+ .same_site(SameSite::Lax)
2526 .max_age(time::Duration::seconds(SESSION_SECONDS))
2627 .build()
2728 }
@@ -21,7 +21,8 @@ pub fn team_cookie(team_id: Uuid) -> Cookie<'static> {
21 .path("/")21 .path("/")
22 .http_only(true)22 .http_only(true)
23 .secure(true)23 .secure(true)
24- .same_site(SameSite::Strict)24+ // Lax comme tt_session : envoyé au lancement d'une PWA Android.
25+ .same_site(SameSite::Lax)
25 .max_age(time::Duration::seconds(SESSION_SECONDS))26 .max_age(time::Duration::seconds(SESSION_SECONDS))
26 .build()27 .build()
27 }28 }