diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 444ef8503..bd9ee8bb2 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -491,13 +491,33 @@ private static String bufferAndHashBodyGenerator(BodyGenerator gen, String hashA private static void append(StringBuilder builder, String name, @Nullable String value, boolean quoted) { builder.append(name).append('='); if (quoted) { - builder.append('"').append(value).append('"'); + builder.append('"'); + appendQuotedStringContent(builder, value); + builder.append('"'); } else { builder.append(value); } builder.append(", "); } + // RFC 7616 params like realm/nonce/opaque are echoed back from the server challenge into the + // quoted-string values of the Authorization header. Backslash-escape " and \ (RFC 7230 quoted-string) + // so a server-supplied value carrying a bare quote cannot close the value early and inject extra + // auth-params into the credentials the client emits. + private static void appendQuotedStringContent(StringBuilder builder, @Nullable String value) { + if (value == null) { + builder.append((String) null); + return; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '"' || c == '\\') { + builder.append('\\'); + } + builder.append(c); + } + } + public static @Nullable String perConnectionProxyAuthorizationHeader(Request request, @Nullable Realm proxyRealm) { String proxyAuthorization = null; if (proxyRealm != null && proxyRealm.isUsePreemptiveAuth()) { diff --git a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java index 0cd3846c8..c98f19483 100644 --- a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java @@ -37,6 +37,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -492,4 +493,29 @@ void computeRspAuth_basic() throws Exception { assertNotNull(rspauth); assertEquals(32, rspauth.length()); // MD5 hex is 32 chars } + + // A server-controlled challenge value (nonce/realm/opaque) must be backslash-escaped when echoed + // into the quoted-string params of the outgoing Authorization header, so a bare " cannot close the + // value early and inject extra auth-params. + @Test + void digestChallengeValuesEscapedInAuthorizationHeader() { + Realm realm = new Realm.Builder("user", "pass") + .setScheme(Realm.AuthScheme.DIGEST) + .setUsePreemptiveAuth(true) + .setRealmName("testrealm") + .setNonce("abc\"x") + .setOpaque("op\\aque") + .setAlgorithm("MD5") + .build(); + Request request = new RequestBuilder("GET") + .setUrl("http://example.com/secret") + .build(); + + String header = AuthenticatorUtils.perRequestAuthorizationHeader(request, realm); + + assertNotNull(header); + assertTrue(header.contains("nonce=\"abc\\\"x\""), header); + assertTrue(header.contains("opaque=\"op\\\\aque\""), header); + assertFalse(header.contains("nonce=\"abc\"x\""), header); + } }