diff --git a/go/webhook_test.go b/go/webhook_test.go index 1c5f2c93a..37f86a13b 100644 --- a/go/webhook_test.go +++ b/go/webhook_test.go @@ -101,6 +101,14 @@ func TestWebhook(t *testing.T) { }, expectedErr: true, }, + { + name: "partial signature is invalid", + testPayload: newTestPayload(time.Now()), + modifyPayload: func(tp *testPayload) { + tp.header.Set("svix-signature", "v1,") + }, + expectedErr: true, + }, { name: "old timestamp fails", testPayload: newTestPayload(time.Now().Add(tolerance * -1)), diff --git a/javascript/src/webhook.test.ts b/javascript/src/webhook.test.ts index 1ef2da7e2..58c5410c8 100644 --- a/javascript/src/webhook.test.ts +++ b/javascript/src/webhook.test.ts @@ -103,6 +103,23 @@ test("invalid signature throws error", () => { }).toThrowError(WebhookVerificationError); }); +test("partial signature throws error", () => { + const wh = new Webhook("MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"); + + const testPayload = new TestPayload(); + testPayload.header["svix-signature"] = testPayload.header["svix-signature"].slice(0, 8); + + expect(() => { + wh.verify(testPayload.payload, testPayload.header); + }).toThrowError(WebhookVerificationError); + + testPayload.header["svix-signature"] = "v1,"; + + expect(() => { + wh.verify(testPayload.payload, testPayload.header); + }).toThrowError(WebhookVerificationError); +}); + test("valid signature is valid and returns valid json", () => { const wh = new Webhook("MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"); diff --git a/rust/src/webhooks.rs b/rust/src/webhooks.rs index 964c17c60..acf8bfe39 100644 --- a/rust/src/webhooks.rs +++ b/rust/src/webhooks.rs @@ -86,10 +86,13 @@ impl Webhook { .filter_map(|x| x.split_once(',')) .filter(|x| x.0 == SIGNATURE_VERSION) .any(|x| { - x.1.bytes() - .zip(expected_signature.bytes()) - .fold(0, |acc, (a, b)| acc | (a ^ b)) - == 0 + (x.1.len() == expected_signature.len()) + && (x + .1 + .bytes() + .zip(expected_signature.bytes()) + .fold(0, |acc, (a, b)| acc | (a ^ b)) + == 0) }) .then_some(()) .ok_or(WebhookError::InvalidSignature) @@ -223,6 +226,43 @@ mod tests { } } + #[test] + fn test_verify_partial_signature() { + let secret = "whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD".to_owned(); + let msg_id = "msg_27UH4WbU6Z5A5EzD8u03UvzRbpk"; + let payload = br#"{"email":"test@example.com","username":"test_user"}"#; + let wh = Webhook::new(&secret).unwrap(); + + let signature = wh + .sign(msg_id, OffsetDateTime::now_utc().unix_timestamp(), payload) + .unwrap(); + + // Just `v1,` + for mut headers in [ + get_svix_headers(msg_id, &signature), + get_unbranded_headers(msg_id, &signature), + ] { + let partial = format!( + "{},", + signature.split(',').collect::>().first().unwrap() + ); + headers.insert(SVIX_MSG_SIGNATURE_KEY, partial.parse().unwrap()); + headers.insert(UNBRANDED_MSG_SIGNATURE_KEY, partial.parse().unwrap()); + assert!(wh.verify(payload, &headers).is_err()); + } + + // Non-empty but still partial signature (first few bytes) + for mut headers in [ + get_svix_headers(msg_id, &signature), + get_unbranded_headers(msg_id, &signature), + ] { + let partial = &signature[0..8]; + headers.insert(SVIX_MSG_SIGNATURE_KEY, partial.parse().unwrap()); + headers.insert(UNBRANDED_MSG_SIGNATURE_KEY, partial.parse().unwrap()); + assert!(wh.verify(payload, &headers).is_err()); + } + } + #[test] fn test_verify_incorrect_timestamp() { let secret = "whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD".to_owned();