From b3fe1d57d13804b6e3352eb767f4cd89ed4011fa Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 15 Aug 2026 17:49:22 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(web):=20accepte=20GET=20/api/project-fi?= =?UTF-8?q?les*=20sans=20Origin=20(allow=5Fmissing=20cibl=C3=A9)=20?= =?UTF-8?q?=E2=80=94=20#214?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/web-server/src/lib.rs | 163 ++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/crates/web-server/src/lib.rs b/crates/web-server/src/lib.rs index 97802a05..7c31e303 100644 --- a/crates/web-server/src/lib.rs +++ b/crates/web-server/src/lib.rs @@ -1241,7 +1241,7 @@ async fn validate_ws_upgrade( ) -> Result<(String, AuthenticatedDevice), Box>> { validate_reverse_proxy(headers, peer_ip, state, WS_PATH)?; - let origin = match validate_request_origin(headers, &state.config) { + let origin = match validate_request_origin(headers, &state.config, false) { Ok(origin) => origin, Err(response) => { state.log_security(SecurityLogEvent::OriginRejected { @@ -1800,7 +1800,11 @@ async fn dispatch_http( return *response; } - let origin = match validate_request_origin(&headers, &state.config) { + let origin = match validate_request_origin( + &headers, + &state.config, + api_route_allows_missing_origin(&method, uri.path()), + ) { Ok(origin) => origin, Err(response) => { state.log_security(SecurityLogEvent::OriginRejected { @@ -4954,8 +4958,12 @@ where fn validate_request_origin( headers: &HeaderMap, config: &ServerConfig, + allow_missing: bool, ) -> Result, Box>> { let Some(origin) = headers.get(ORIGIN).and_then(|value| value.to_str().ok()) else { + if allow_missing { + return Ok(None); + } return Err(Box::new(error_response( StatusCode::FORBIDDEN, "FORBIDDEN", @@ -4976,6 +4984,16 @@ fn validate_request_origin( } } +fn api_route_allows_missing_origin(method: &Method, path: &str) -> bool { + method == Method::GET + && matches!( + path, + "/api/project-files" + | "/api/project-files/download-file" + | "/api/project-files/download-directory" + ) +} + fn validate_reverse_proxy( headers: &HeaderMap, peer_ip: Option, @@ -6052,6 +6070,29 @@ mod tests { .await } + async fn request_without_origin( + state: Arc, + method: Method, + uri: &str, + body: Value, + extra_headers: &[(&str, &str)], + ) -> Response { + let mut builder = Request::builder() + .method(method) + .uri(uri) + .header(CONTENT_TYPE, "application/json"); + for (name, value) in extra_headers { + builder = builder.header(HeaderName::from_bytes(name.as_bytes()).unwrap(), *value); + } + handle_request( + builder + .body(Full::new(Bytes::from(serde_json::to_vec(&body).unwrap()))) + .unwrap(), + state, + ) + .await + } + async fn response_json(response: Response) -> (StatusCode, Value, HeaderMap) { let status = response.status(); let headers = response.headers().clone(); @@ -6570,6 +6611,20 @@ mod tests { assert_eq!(response.status(), StatusCode::FORBIDDEN); } + #[tokio::test] + async fn websocket_upgrade_requires_origin_header() { + let state = state(); + let cookie = pair_and_cookie(Arc::clone(&state)).await; + let mut headers = ws_headers("http://127.0.0.1:17373", Some(&cookie)); + headers.remove(ORIGIN); + + let response = validate_ws_upgrade(&headers, Some("127.0.0.1".parse().unwrap()), &state) + .await + .unwrap_err(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } + #[tokio::test] async fn websocket_upgrade_accepts_valid_cookie_and_origin() { let state = state(); @@ -7309,6 +7364,26 @@ mod tests { assert!(without_proxy.validate().is_err()); } + #[tokio::test] + async fn post_pair_requires_origin_header() { + let state = state(); + + let (status, body, _) = response_json( + request_without_origin( + state, + Method::POST, + "/api/pair", + json!({ "code": "PAIR1234", "name": "Test device" }), + &[], + ) + .await, + ) + .await; + + assert_eq!(status, StatusCode::FORBIDDEN); + assert_eq!(body["message"], "missing Origin header"); + } + #[tokio::test] async fn remote_bind_rejects_peer_when_no_authorized_proxy_is_configured() { let logger = Arc::new(RecordingSecurityLogger::default()); @@ -8541,6 +8616,90 @@ mod tests { assert!(!entries.iter().any(|entry| entry["name"] == ".ideai")); } + #[tokio::test] + async fn authorized_project_file_gets_allow_missing_origin_header() { + let state = state(); + let project_id = create_project_for_test(&state, "Project Files No Origin").await; + let project = resolve_project_readonly(&project_id, &state.app) + .await + .expect("project resolves"); + std::fs::create_dir_all(format!("{}/src", project.root.as_str())).unwrap(); + std::fs::write( + format!("{}/src/main.rs", project.root.as_str()), + "fn main() {}\n", + ) + .unwrap(); + let cookie = pair_and_cookie(Arc::clone(&state)).await; + + let (list_status, list_body, _) = response_json( + request_without_origin( + Arc::clone(&state), + Method::GET, + &format!("/api/project-files?projectId={project_id}&path="), + json!({}), + &[("cookie", &cookie)], + ) + .await, + ) + .await; + let (file_status, file_body, _) = response_bytes( + request_without_origin( + Arc::clone(&state), + Method::GET, + &format!( + "/api/project-files/download-file?projectId={project_id}&path=src/main.rs" + ), + json!({}), + &[("cookie", &cookie)], + ) + .await, + ) + .await; + let (directory_status, directory_body, _) = response_bytes( + request_without_origin( + Arc::clone(&state), + Method::GET, + &format!("/api/project-files/download-directory?projectId={project_id}&path=src"), + json!({}), + &[("cookie", &cookie)], + ) + .await, + ) + .await; + + assert_eq!(list_status, StatusCode::OK); + assert!(list_body["entries"] + .as_array() + .unwrap() + .iter() + .any(|entry| entry["path"] == "src")); + assert_eq!(file_status, StatusCode::OK); + assert_eq!(file_body, Bytes::from_static(b"fn main() {}\n")); + assert_eq!(directory_status, StatusCode::OK); + assert!(directory_body.starts_with(b"PK")); + } + + #[tokio::test] + async fn non_get_project_file_route_still_requires_origin_header() { + let state = state(); + let project_id = create_project_for_test(&state, "Project Files POST No Origin").await; + + let (status, body, _) = response_json( + request_without_origin( + state, + Method::POST, + &format!("/api/project-files?projectId={project_id}&path="), + json!({}), + &[], + ) + .await, + ) + .await; + + assert_eq!(status, StatusCode::FORBIDDEN); + assert_eq!(body["message"], "missing Origin header"); + } + #[tokio::test] async fn authorized_project_files_rejects_traversal_and_hidden_paths() { let state = state(); From 76f22026011c957d7f954401c5f8de457a0d23fd Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 15 Aug 2026 17:49:22 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(idea):=20bookkeeping=20#214=20QA=20ve?= =?UTF-8?q?rte=20+=20cl=C3=B4tures=20index=20#212/#213?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ideai/tickets/212/carnet.md | 4 ++-- .ideai/tickets/212/issue.md | 6 +++--- .ideai/tickets/213/carnet.md | 13 +++++++++++++ .ideai/tickets/213/issue.md | 17 +++++++++++++++++ .ideai/tickets/214/carnet.md | 29 +++++++++++++++++++++++++++++ .ideai/tickets/214/issue.md | 17 +++++++++++++++++ .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 30 +++++++++++++++++++++++++++++- 8 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 .ideai/tickets/213/carnet.md create mode 100644 .ideai/tickets/213/issue.md create mode 100644 .ideai/tickets/214/carnet.md create mode 100644 .ideai/tickets/214/issue.md diff --git a/.ideai/tickets/212/carnet.md b/.ideai/tickets/212/carnet.md index 17d1fc35..b505cf09 100644 --- a/.ideai/tickets/212/carnet.md +++ b/.ideai/tickets/212/carnet.md @@ -1,8 +1,8 @@ --- issueRef: "#212" -version: 5 +version: 6 updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} -updatedAt: 1786786758650 +updatedAt: 1786786870793 --- 2026-08-15 Main triage + implementation + QA. - Symptome confirme par lecture code: frontend web invoque `load_layout`, mais `crates/web-server/src/lib.rs` ne route pas cette commande et tombe sur `UNKNOWN_COMMAND`. diff --git a/.ideai/tickets/212/issue.md b/.ideai/tickets/212/issue.md index 1ae5a867..40700dd3 100644 --- a/.ideai/tickets/212/issue.md +++ b/.ideai/tickets/212/issue.md @@ -2,7 +2,7 @@ id: "1dbe01ec-94f4-404d-9ffb-2ceeceb0557b" number: 212 title: "Web PC: erreur `unknown command: load_layout` a l'ouverture d'un projet" -status: "qa" +status: "closed" priority: "high" sprint: null links: [] @@ -11,7 +11,7 @@ attachments: [] createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} createdAt: 1786786004974 -updatedAt: 1786786758650 -version: 5 +updatedAt: 1786786870793 +version: 6 --- A l'ouverture d'un projet dans l'interface web desktop/PC, l'application affiche `unknown command: load_layout`. Triage et correctif a produire avec cycle UX/Architect/Git/Dev/QA. \ No newline at end of file diff --git a/.ideai/tickets/213/carnet.md b/.ideai/tickets/213/carnet.md new file mode 100644 index 00000000..2a31a6c1 --- /dev/null +++ b/.ideai/tickets/213/carnet.md @@ -0,0 +1,13 @@ +--- +issueRef: "#213" +version: 3 +updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +updatedAt: 1786787447568 +--- +2026-08-15 rebuild AppImage. +- Skill `build-appimage` lu et applique. +- Branche/commit au lancement: `develop` @ `b3bf6fd2d64990639cef22fbb2a0e5f35579aabc`. +- Frontend build OK via `npm --prefix frontend run build`. +- Bundle AppImage OK via `CARGO_HOME=/tmp/idea-cargo-home APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 ../../frontend/node_modules/.bin/tauri build --bundles appimage` depuis `crates/app-tauri/`. +- Artefact produit: `target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`. +- Etape restante: tagging Git du commit buildé selon la regle branche->version (`develop` => increment Y). \ No newline at end of file diff --git a/.ideai/tickets/213/issue.md b/.ideai/tickets/213/issue.md new file mode 100644 index 00000000..a8d46eb1 --- /dev/null +++ b/.ideai/tickets/213/issue.md @@ -0,0 +1,17 @@ +--- +id: "f0bfa4e0-8f1d-4269-a7f5-422090d7c79d" +number: 213 +title: "Rebuild AppImage Linux" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +attachments: [] +createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +createdAt: 1786787271504 +updatedAt: 1786787447568 +version: 3 +--- +Reconstruction de l'AppImage IdeA Linux via le workflow valide, puis tagging Git de la version du commit buildé selon la branche courante. \ No newline at end of file diff --git a/.ideai/tickets/214/carnet.md b/.ideai/tickets/214/carnet.md new file mode 100644 index 00000000..fc152b3a --- /dev/null +++ b/.ideai/tickets/214/carnet.md @@ -0,0 +1,29 @@ +--- +issueRef: "#214" +version: 5 +updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +updatedAt: 1786808846046 +--- +## Triage 2026-08-15 +- Symptôme: sur l'interface web PC, l'accès aux fichiers échoue avec `missing Origin header`. +- Diagnostic convergent UX/Architect: rejet backend `403` sur validation `Origin` appliquée aux routes `/api/*`, y compris des `GET` fichiers browser-safe. +- Branche locale créée par Git: `fix/214-web-origin-header-file-access` depuis `develop`. +- Direction de fix: conserver le contrôle strict `Origin` pour `POST`/`OPTIONS`/WS, mais accepter les `GET` lecture/fichiers sans header `Origin` et ajouter des tests de non-régression. +- UX: si un message utilisateur reste nécessaire, ne pas exposer le texte technique brut `missing Origin header`. + +## Implémentation DevBackend +- Fichier modifié: `crates/web-server/src/lib.rs`. +- Politique livrée: `GET /api/project-files`, `/api/project-files/download-file`, `/api/project-files/download-directory` acceptés sans `Origin`; validation stricte maintenue si `Origin` est présent. +- Non-régression attendue: `POST /api/pair` sans `Origin` toujours refusé, WebSocket sans `Origin` toujours refusé. +- Tests DevBackend: + - `cargo fmt --package web-server` => OK + - `cargo test -p web-server origin_header` => OK (3 tests) + - `cargo test -p web-server authorized_project_file_gets_allow_missing_origin_header` => OK + - `cargo test -p web-server` => KO (`Too many open files`) + - `cargo test -p web-server -- --test-threads=1` => échecs PTY/WebSocket non reliés au correctif Origin selon DevBackend + +## Verdict QA +- QA a ajouté un test négatif complémentaire pour verrouiller qu'une route fichier non-GET sans `Origin` reste refusée. +- Commande validante principale: `cargo test -p web-server origin_header -- --nocapture` => VERT (4/4) +- Rejeu ciblé: `cargo test -p web-server non_get_project_file_route_still_requires_origin_header -- --nocapture` => VERT +- Conclusion: `GET /api/project-files*` sans `Origin` accepté; `POST /api/pair`, WebSocket et route fichier non-GET sans `Origin` refusés. diff --git a/.ideai/tickets/214/issue.md b/.ideai/tickets/214/issue.md new file mode 100644 index 00000000..5c385790 --- /dev/null +++ b/.ideai/tickets/214/issue.md @@ -0,0 +1,17 @@ +--- +id: "05b7dc82-f248-476d-8ae6-c5190b2d9d02" +number: 214 +title: "Bug web PC: accès fichiers échoue avec 'missing Origin header'" +status: "qa" +priority: "high" +sprint: null +links: [] +agentRefs: [] +attachments: [] +createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +createdAt: 1786808303548 +updatedAt: 1786808846046 +version: 5 +--- +Sur l'interface web PC, l'accès aux fichiers échoue avec l'erreur 'missing Origin header'. Objectif: identifier la cause, corriger le flux web desktop concerné, puis valider par tests QA réels. \ No newline at end of file diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json index b6e6765b..9e222dfb 100644 --- a/.ideai/tickets/counter.json +++ b/.ideai/tickets/counter.json @@ -1,3 +1,3 @@ { - "nextNumber": 213 + "nextNumber": 215 } \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index b24667aa..f354e602 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -2862,6 +2862,34 @@ "issueRef": "#212", "path": "212", "title": "Web PC: erreur `unknown command: load_layout` a l'ouverture d'un projet", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" + }, + "updatedAt": 1786786870793 + }, + { + "issueRef": "#213", + "path": "213", + "title": "Rebuild AppImage Linux", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" + }, + "updatedAt": 1786787447568 + }, + { + "issueRef": "#214", + "path": "214", + "title": "Bug web PC: accès fichiers échoue avec 'missing Origin header'", "status": "qa", "priority": "high", "sprint": null, @@ -2870,7 +2898,7 @@ "kind": "agent", "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" }, - "updatedAt": 1786786758650 + "updatedAt": 1786808846046 } ] } \ No newline at end of file