Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Reject ASCII-whitespace-only webhook secrets
pr_url: https://github.com/stripe/stripe-java/pull/2313
semver_level: patch
jira_tickets_closed:
- RUN_DEVSDK-3378
---

- Reject webhook signing secrets made entirely of ASCII whitespace instead of using them as HMAC keys.
21 changes: 20 additions & 1 deletion src/main/java/com/stripe/StripeEventNotificationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,31 @@ public StripeEventNotificationHandler(
StripeClient client,
EventNotificationFallbackCallback fallbackCallback) {
super(client, fallbackCallback);
if (webhookSecret == null || webhookSecret.isEmpty()) {
if (isBlankWebhookSecret(webhookSecret)) {
throw new IllegalArgumentException("webhookSecret must be a non-empty string");
}
this.webhookSecret = webhookSecret;
}

private static boolean isBlankWebhookSecret(String secret) {
if (secret == null || secret.isEmpty()) {
return true;
}

for (int i = 0; i < secret.length(); i++) {
char character = secret.charAt(i);
if (character != ' '
&& character != '\t'
&& character != '\r'
&& character != '\n'
&& character != '\f'
&& character != '\u000B') {
return false;
}
}
return true;
}

/**
* Creates a handler that processes events without webhook signature verification. Intended for
* pre-authenticated channels like AWS EventBridge or Azure Event Grid.
Expand Down
21 changes: 20 additions & 1 deletion src/main/java/com/stripe/net/Webhook.java
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public static boolean verifyHeader(
"No signatures found with expected scheme", sigHeader);
}

if (secret == null || secret.isEmpty()) {
if (isBlankWebhookSecret(secret)) {
throw new SignatureVerificationException(
"No webhook secret value was provided. It should start with `whsec_`", sigHeader);
}
Expand Down Expand Up @@ -311,6 +311,25 @@ private static List<String> getSignatures(String sigHeader, String scheme) {
return signatures;
}

private static boolean isBlankWebhookSecret(String secret) {
if (secret == null || secret.isEmpty()) {
return true;
}

for (int i = 0; i < secret.length(); i++) {
char character = secret.charAt(i);
if (character != ' '
&& character != '\t'
&& character != '\r'
&& character != '\n'
&& character != '\f'
&& character != '\u000B') {
return false;
}
}
return true;
}

/**
* Computes the signature for a given payload and secret.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,58 @@ public void testConstructor_rejectsEmptySecret() {
() -> new StripeEventNotificationHandler("", stripeClient, fallbackCallback));
}

@Test
public void testConstructor_rejectsWhitespaceOnlySecrets() {
String[] blankSecrets = {" ", "\t", "\r", "\n", "\f", "\u000B", " \t\r\n\f\u000B"};

for (String blankSecret : blankSecrets) {
IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() ->
new StripeEventNotificationHandler(blankSecret, stripeClient, fallbackCallback));
assertEquals("webhookSecret must be a non-empty string", exception.getMessage());
}
}

@Test
public void testConstructor_preservesSecretWithSurroundingWhitespace()
throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
String secretWithWhitespace = " \twhsec_test_secret\r\n";
StripeEventNotificationHandler handler =
new StripeEventNotificationHandler(secretWithWhitespace, stripeClient, fallbackCallback);
Map<String, Object> options = new HashMap<>();
options.put("payload", v1BillingMeterPayload);
options.put("secret", secretWithWhitespace);

handler.handle(v1BillingMeterPayload, generateSigHeader(options));

verify(fallbackCallback, times(1))
.process(
org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any());
}

@Test
public void testConstructor_acceptsNonBreakingSpaceOnlySecret()
throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
String nonBreakingSpaceSecret = "\u00A0";
StripeEventNotificationHandler handler =
new StripeEventNotificationHandler(nonBreakingSpaceSecret, stripeClient, fallbackCallback);
Map<String, Object> options = new HashMap<>();
options.put("payload", v1BillingMeterPayload);
options.put("secret", nonBreakingSpaceSecret);

handler.handle(v1BillingMeterPayload, generateSigHeader(options));

verify(fallbackCallback, times(1))
.process(
org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any());
}

@Test
public void testWithoutVerification_staticFactory() {
// Test that StripeEventNotificationHandler.withoutVerification(...) returns the correct type.
Expand Down
36 changes: 36 additions & 0 deletions src/test/java/com/stripe/net/WebhookTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,42 @@ public void testNullSecret()
exception.getMessage());
}

@Test
public void testWhitespaceOnlySecrets() throws NoSuchAlgorithmException, InvalidKeyException {
String[] blankSecrets = {" ", "\t", "\r", "\n", "\f", "\u000B", " \t\r\n\f\u000B"};

for (String blankSecret : blankSecrets) {
final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, blankSecret);
Throwable exception =
assertThrows(
SignatureVerificationException.class,
() -> Webhook.Signature.verifyHeader(payload, sigHeader, blankSecret, 0, null));
assertEquals(
"No webhook secret value was provided. It should start with `whsec_`",
exception.getMessage());
}
}

@Test
public void testSecretWithSurroundingWhitespaceIsNotNormalized()
throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
final String secretWithWhitespace = " \twhsec_test_secret\r\n";
final String sigHeader =
Webhook.Signature.generateSignatureHeader(payload, secretWithWhitespace);

assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secretWithWhitespace, 0, null));
}

@Test
public void testNonBreakingSpaceOnlySecretIsNotBlank()
throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
final String nonBreakingSpaceSecret = "\u00A0";
final String sigHeader =
Webhook.Signature.generateSignatureHeader(payload, nonBreakingSpaceSecret);

assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, nonBreakingSpaceSecret, 0, null));
}

@Test
public void testNoValidSignatureForPayload()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
Expand Down
Loading