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
Expand Up @@ -97,7 +97,20 @@ public object AuthChallengeParser {
// param (likely the next challenge's scheme).
while (true) {
cursor.skipOws()
if (!cursor.hasMore() || cursor.peek() != ',') break
if (!cursor.hasMore()) break
if (cursor.peek() != ',') {
// After a valid auth-param the grammar only permits a comma
// (another param or the next challenge) or end-of-input. Anything
// else is a stray trailing token with no separating comma — e.g.
// `Bearer realm="x" garbage`. RFC 7235 §2.1 has no production for
// it, so the tail is malformed. Skip it to the next top-level
// comma so it is not silently misread as a phantom second
// challenge's scheme on the next outer iteration, then emit this
// challenge with the params parsed before the garbage — matching
// the parser's lenient "preserve prior params" recovery contract.
cursor.recoverToNextChallenge()
break
}
// Save position before consuming the comma — if what follows is the
// next scheme rather than a param of THIS challenge, we need to leave
// the comma in place for the outer loop.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,49 @@ class AuthChallengeParserTest {
)
}

@Test
fun `stray token after an unquoted param is rejected not parsed as a phantom challenge`() {
// `Digest realm=value extra` — `realm=value` is a valid auth-param, but `extra`
// is a bare token with no separating comma. RFC 7235 §2.1 permits only a comma
// (or EOF) after an auth-param, so the trailing token is malformed. The parser
// skips it (rather than silently dropping it and re-reading `extra` as the scheme
// of a second challenge) and emits the single Digest challenge with `realm=value`.
val challenges = AuthChallengeParser.parse("Digest realm=value extra")
assertEquals(1, challenges.size, "the stray token must not produce a second challenge")
assertEquals("digest", challenges[0].scheme)
assertEquals("value", challenges[0].parameters["realm"])
assertTrue(
challenges.none { it.scheme == "extra" },
"the stray trailing token must not be parsed as a phantom challenge scheme",
)
}

@Test
fun `stray token after a quoted param is rejected not parsed as a phantom challenge`() {
// Quoted-value variant of the stray-trailing-token case: `Digest realm="value" extra`.
val challenges = AuthChallengeParser.parse("""Digest realm="value" extra""")
assertEquals(1, challenges.size, "the stray token must not produce a second challenge")
assertEquals("digest", challenges[0].scheme)
assertEquals("value", challenges[0].parameters["realm"])
assertTrue(
challenges.none { it.scheme == "extra" },
"the stray trailing token must not be parsed as a phantom challenge scheme",
)
}

@Test
fun `stray token between a valid param and a comma-separated next challenge is dropped`() {
// `Digest realm=value extra, Basic realm="x"` — the stray `extra` after the
// first challenge's param is skipped up to the next top-level comma, and the
// following Basic challenge is still picked up cleanly.
val challenges = AuthChallengeParser.parse("""Digest realm=value extra, Basic realm="x"""")
assertEquals(2, challenges.size)
assertEquals("digest", challenges[0].scheme)
assertEquals("value", challenges[0].parameters["realm"])
assertEquals("basic", challenges[1].scheme)
assertEquals("x", challenges[1].parameters["realm"])
}

@Test
fun `quote with only a backslash inside before close quote`() {
// `"\"` — opens, sees backslash, advances, sees `"` (the close), appends
Expand Down
Loading