CVE-2024-21491 (Rust, published 2024-02-13T05:15Z)
Description: value: Versions of the package svix before 1.17.0 are vulnerable to Authentication Bypass due to an issue in the verify function where signatures of different lengths are incorrectly compared. An attacker can bypass signature verification by providing a shorter signature that matches the beginning of the actual signature.**Note:**The attacker would need to know a victim uses the Rust library for verification,no easy way to automatically check that; and uses webhooks by a service that uses Svix, and then figure out a way to craft a malicious payload that will actually include all of the correct identifiers needed to trick the receivers to cause actual issues.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/svix/svix-webhooks
Files changed:
go/webhook_test.go
javascript/src/webhook.test.ts
rust/src/webhooks.rs
Commit message: Rust verification library: fix comparison of different length signatures (#1190)

There was a bug in the code which meant that the signatures are only
compared up to the length of the shorter signature, which means that an
attacker can just pass `v1,` as the signature and that will always pass
verification.

This change fixes it so that the length of the signature is also taken
into account when comparing, to make sure that it's always the same
length before comparing.

Manually verified all of the other libraries are correct, and added
tests to JavaScript and Go (even though they were also not affected).

Many thanks to Fredrik Meringdal (@fmeringdal) for the report.

Vulnerable code:
                x.1.bytes()
                    .zip(expected_signature.bytes())
                    .fold(0, |acc, (a, b)| acc | (a ^ b))
                    == 0

Fixed code:
		{
			name:        "partial signature is invalid",
			testPayload: newTestPayload(time.Now()),
			modifyPayload: func(tp *testPayload) {
				tp.header.Set("svix-signature", "v1,")
			},
			expectedErr: true,
		},
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);
});

                (x.1.len() == expected_signature.len())
                    && (x
                        .1
                        .bytes()
                        .zip(expected_signature.bytes())
                        .fold(0, |acc, (a, b)| acc | (a ^ b))
                        == 0)
    #[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::<Vec<&str>>().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());
        }
    }