diff --git a/src/main/java/com/stripe/mpp/Parsing.java b/src/main/java/com/stripe/mpp/Parsing.java index ada619c..e049411 100644 --- a/src/main/java/com/stripe/mpp/Parsing.java +++ b/src/main/java/com/stripe/mpp/Parsing.java @@ -63,7 +63,10 @@ static Map parseAuthParams(String input) { } private static String quote(String value) { - return "\"" + value + "\""; + if (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0) { + throw new IllegalArgumentException("Header values must not contain CR or LF"); + } + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; } // --- WWW-Authenticate (Challenge) --- diff --git a/src/test/java/com/stripe/mpp/ParsingTest.java b/src/test/java/com/stripe/mpp/ParsingTest.java index 500ab88..67546e9 100644 --- a/src/test/java/com/stripe/mpp/ParsingTest.java +++ b/src/test/java/com/stripe/mpp/ParsingTest.java @@ -46,6 +46,42 @@ void challengeFromWwwAuthenticateHandlesMultipleSchemes() { assertThat(challenges.get(0).id()).isEqualTo("abc"); } + @Test + void challengeEscapesQuotedStringDescription() { + Challenge original = Challenge.create("secret", "example.com", "tempo", "charge", Map.of(), + null, "Pay \"premium\" API", null); + + String header = original.toWwwAuthenticate(); + + assertThat(header).contains("description=\"Pay \\\"premium\\\" API\""); + var parsed = Challenge.fromWwwAuthenticate(header); + assertThat(parsed).hasSize(1); + assertThat(parsed.get(0).description()).isEqualTo("Pay \"premium\" API"); + } + + @Test + void challengeEscapesBackslashDescription() { + Challenge original = Challenge.create("secret", "example.com", "tempo", "charge", Map.of(), + null, "Path C:\\tempo\\api", null); + + String header = original.toWwwAuthenticate(); + + assertThat(header).contains("description=\"Path C:\\\\tempo\\\\api\""); + var parsed = Challenge.fromWwwAuthenticate(header); + assertThat(parsed).hasSize(1); + assertThat(parsed.get(0).description()).isEqualTo("Path C:\\tempo\\api"); + } + + @Test + void challengeRejectsCrlfDescription() { + Challenge challenge = Challenge.create("secret", "example.com", "tempo", "charge", Map.of(), + null, "Line one\r\nLine two", null); + + assertThatThrownBy(challenge::toWwwAuthenticate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("CR or LF"); + } + // --- Credential (Authorization) --- @Test