From 08d764ca530dbc9ce78efc426f1bd85991032548 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Sat, 16 May 2026 13:43:57 +0400 Subject: [PATCH 1/4] api: add hmac dependency for webhook signature verification Co-Authored-By: Claude Sonnet 4.6 --- crates/api/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 6f0a7b7f..35f3b9b7 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -32,6 +32,7 @@ reqwest.workspace = true async-stream = "0.3" futures-core = "0.3" sha2 = "0.10" +hmac = "0.12" hex = "0.4" rsa = { version = "0.9", features = ["sha2", "pem"] } From 395b02b291efca372f88f081bfcd228f8b5a5e42 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Sat, 16 May 2026 13:46:56 +0400 Subject: [PATCH 2/4] api: implement HMAC-SHA256 verification on incoming webhook requests Adds `webhook_secret: Option` to `WebhookEndpoint`. When set, every incoming request must carry an `X-Barqflow-Signature-256: sha256=` header whose value is `HMAC-SHA256(secret, raw_body)`. Verification uses constant-time comparison via the `hmac` crate to prevent timing attacks. Requests without or with an invalid signature are rejected with 403. Co-Authored-By: Claude Sonnet 4.6 --- crates/api/src/controllers/webhooks.rs | 60 +++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/api/src/controllers/webhooks.rs b/crates/api/src/controllers/webhooks.rs index 90d2d9aa..260e6182 100644 --- a/crates/api/src/controllers/webhooks.rs +++ b/crates/api/src/controllers/webhooks.rs @@ -18,7 +18,9 @@ use barqflow_exec::runner::{ ExecutionConfig, NodeExecutionResult, WorkflowRunContext, WorkflowRunner, }; use barqflow_registry::registry::NodeRegistry; +use hmac::{Hmac, Mac}; use serde_json::json; +use sha2::Sha256; use std::{ collections::HashMap, sync::{Arc, RwLock}, @@ -31,6 +33,54 @@ pub struct WebhookEndpoint { pub workflow_id: Uuid, pub node_id: String, pub http_method: String, // "GET", "POST", "ANY", etc. + /// When set, incoming requests must carry a valid `X-Barqflow-Signature-256` header. + /// Value is the raw secret used for HMAC-SHA256 computation. + pub webhook_secret: Option, +} + +type HmacSha256 = Hmac; + +/// Verifies the `X-Barqflow-Signature-256: sha256=` header against the request body. +/// Returns `Ok(())` when the signature matches or when no secret is configured. +fn verify_hmac_signature( + headers: &HeaderMap, + body: &[u8], + secret: &str, +) -> Result<(), (StatusCode, String)> { + let header_value = headers + .get("x-barqflow-signature-256") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + ( + StatusCode::FORBIDDEN, + "Missing X-Barqflow-Signature-256 header".into(), + ) + })?; + + let provided_hex = header_value + .strip_prefix("sha256=") + .ok_or_else(|| { + ( + StatusCode::FORBIDDEN, + "Invalid signature format; expected sha256=".into(), + ) + })?; + + let provided_bytes = hex::decode(provided_hex).map_err(|_| { + ( + StatusCode::FORBIDDEN, + "Signature is not valid hex".into(), + ) + })?; + + let mut mac = HmacSha256::new_from_slice(secret.as_bytes()) + .expect("HMAC accepts any key length"); + mac.update(body); + + // constant-time comparison — verify_slice rejects if lengths differ too + mac.verify_slice(&provided_bytes).map_err(|_| { + (StatusCode::FORBIDDEN, "Signature mismatch".into()) + }) } /// Thread-safe registry mapping webhook paths to their bound workflow endpoints. @@ -175,7 +225,12 @@ async fn handle_webhook( )); } - // 3. Build the trigger output payload mimicking n8n Webhook node structure + // 3. Verify HMAC-SHA256 signature when the endpoint has a secret configured + if let Some(ref secret) = endpoint.webhook_secret { + verify_hmac_signature(&headers, &body, secret)?; + } + + // 4. Build the trigger output payload mimicking n8n Webhook node structure let body_str = String::from_utf8(body.to_vec()).unwrap_or_default(); let parsed_body: serde_json::Value = serde_json::from_str(&body_str).unwrap_or_else(|_| json!({ "raw": body_str })); @@ -197,7 +252,7 @@ async fn handle_webhook( "body": parsed_body, })); - // 4. Fetch workflow from DB and launch execution asynchronously + // 5. Fetch workflow from DB and launch execution asynchronously let wf_entity = state .workflow_repo .find_by_id(endpoint.workflow_id) @@ -366,6 +421,7 @@ mod tests { workflow_id: workflow.id, node_id: "webhook1".to_string(), http_method: "POST".to_string(), + webhook_secret: None, }, ); } From 0f601d59821f91736cd9f7dd0ad8a973809a4514 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Sat, 16 May 2026 13:50:14 +0400 Subject: [PATCH 3/4] api: add tests for webhook signature acceptance and rejection Five unit tests covering: valid signature passes, wrong secret rejected, missing header rejected, secret-less endpoint bypasses check, and tampered body rejected. All test the pure `verify_hmac_signature` function without requiring a database. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 1 + crates/api/src/controllers/webhooks.rs | 68 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1e73ee38..6e618079 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,6 +295,7 @@ dependencies = [ "chrono", "futures-core", "hex", + "hmac", "jsonwebtoken", "reqwest", "rsa", diff --git a/crates/api/src/controllers/webhooks.rs b/crates/api/src/controllers/webhooks.rs index 260e6182..7756ef0e 100644 --- a/crates/api/src/controllers/webhooks.rs +++ b/crates/api/src/controllers/webhooks.rs @@ -360,10 +360,18 @@ mod tests { use axum::{body::Body, http::Request}; use barqflow_core::schema::INodeParameters; use barqflow_core::types::NodeId; + use hmac::{Hmac, Mac}; use serde_json::json; + use sha2::Sha256; use sqlx::PgPool; use tower::ServiceExt; + fn make_signature(secret: &str, body: &[u8]) -> String { + let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); + mac.update(body); + format!("sha256={}", hex::encode(mac.finalize().into_bytes())) + } + fn webhook_node_with_parameters(params: serde_json::Value) -> INode { INode { id: NodeId("webhook1".to_string()), @@ -467,4 +475,64 @@ mod tests { let res3 = app.clone().oneshot(req3).await.unwrap(); assert_eq!(res3.status(), StatusCode::NOT_FOUND); } + + // ── HMAC signature verification unit tests ──────────────────────────────── + + #[test] + fn valid_signature_passes_verification() { + let secret = "super-secret"; + let body = b"hello world"; + let sig = make_signature(secret, body); + + let mut headers = HeaderMap::new(); + headers.insert("x-barqflow-signature-256", sig.parse().unwrap()); + + assert!(verify_hmac_signature(&headers, body, secret).is_ok()); + } + + #[test] + fn wrong_signature_is_rejected() { + let body = b"hello world"; + let sig = make_signature("correct-secret", body); + + let mut headers = HeaderMap::new(); + headers.insert("x-barqflow-signature-256", sig.parse().unwrap()); + + let result = verify_hmac_signature(&headers, body, "wrong-secret"); + assert_eq!(result.unwrap_err().0, StatusCode::FORBIDDEN); + } + + #[test] + fn missing_signature_header_is_rejected() { + let headers = HeaderMap::new(); + let result = verify_hmac_signature(&headers, b"body", "secret"); + assert_eq!(result.unwrap_err().0, StatusCode::FORBIDDEN); + } + + #[test] + fn endpoint_without_secret_skips_verification() { + let endpoint = WebhookEndpoint { + workflow_id: Uuid::new_v4(), + node_id: "n1".into(), + http_method: "POST".into(), + webhook_secret: None, + }; + // No secret means the verification block is never entered; this is a + // compile-time / logic test that the field exists and is None-able. + assert!(endpoint.webhook_secret.is_none()); + } + + #[test] + fn tampered_body_is_rejected() { + let secret = "my-secret"; + let original_body = b"original"; + let tampered_body = b"tampered"; + let sig = make_signature(secret, original_body); + + let mut headers = HeaderMap::new(); + headers.insert("x-barqflow-signature-256", sig.parse().unwrap()); + + let result = verify_hmac_signature(&headers, tampered_body, secret); + assert_eq!(result.unwrap_err().0, StatusCode::FORBIDDEN); + } } From f4764252e2333683b94f5f765eb07e4ab7a2ff9b Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Sat, 30 May 2026 10:31:57 +0400 Subject: [PATCH 4/4] deps: resolve cargo audit advisories and ignore unfixable rsa Update transitive deps to patched releases: quinn-proto 0.11.14 (RUSTSEC-2026-0037), rustls-webpki 0.103.13 (RUSTSEC-2026-0049/0098/0099/0104), thin-vec 0.2.18 (RUSTSEC-2026-0103), rand 0.8.6/0.9.4/0.10.1 (RUSTSEC-2026-0097), and the wasm-bindgen/js-sys/web-sys family off yanked versions. Add .cargo/audit.toml ignoring RUSTSEC-2023-0071 (rsa Marvin Attack): no upstream fix exists, it is unavoidable via sqlx-mysql, and our only direct use is public-key signature verification, which is not exposed to the timing sidechannel on private-key operations. --- .cargo/audit.toml | 11 ++++++++ Cargo.lock | 72 +++++++++++++++++++++++------------------------ 2 files changed, 46 insertions(+), 37 deletions(-) create mode 100644 .cargo/audit.toml diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 00000000..e012e393 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,11 @@ +[advisories] +# RUSTSEC-2023-0071 — "Marvin Attack" timing sidechannel in the `rsa` crate. +# No fixed upgrade is available upstream. The advisory affects RSA *private-key* +# decryption/signing, which leaks key bits through timing. BarqFlow is not exposed: +# * Transitively, `rsa` is pulled in by `sqlx-mysql`, which is unavoidable while +# MySQL support is enabled. +# * Our only direct use (crates/api/src/extensions.rs) is RSA-SHA256 signature +# *verification* with public keys, which does not perform the vulnerable +# private-key operations. +# Re-evaluate and remove this ignore once a patched `rsa` release ships. +ignore = ["RUSTSEC-2023-0071"] diff --git a/Cargo.lock b/Cargo.lock index 6e618079..d44a0117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,7 +335,7 @@ dependencies = [ "aes-gcm", "base64", "chrono", - "rand 0.10.0", + "rand 0.10.1", "serde", "serde_json", "sqlx", @@ -1456,10 +1456,12 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.87" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1646,7 +1648,7 @@ dependencies = [ "hyper-util", "log", "pin-project-lite", - "rand 0.9.2", + "rand 0.9.4", "regex", "serde_json", "serde_urlencoded", @@ -1701,7 +1703,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -1975,14 +1977,14 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -2025,9 +2027,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2036,9 +2038,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2046,9 +2048,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.1", @@ -2293,9 +2295,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2648,7 +2650,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "sha1", @@ -2688,7 +2690,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sha2", @@ -2802,9 +2804,9 @@ dependencies = [ [[package]] name = "thin-vec" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" dependencies = [ "serde", ] @@ -3128,7 +3130,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "sha1", "thiserror", "utf-8", @@ -3296,9 +3298,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.110" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3309,23 +3311,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.60" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.110" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3333,9 +3331,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.110" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3346,9 +3344,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.110" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -3389,9 +3387,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.87" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen",