Skip to content

test(sso/spnego): pin the Basic realm check against the library's own parser - #3264

Open
marevol wants to merge 1 commit into
masterfrom
test/spnego-basic-realm-library-parity
Open

test(sso/spnego): pin the Basic realm check against the library's own parser#3264
marevol wants to merge 1 commit into
masterfrom
test/spnego-basic-realm-library-parity

Conversation

@marevol

@marevol marevol commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What this guards

On the Basic path, spnego.allowed.realms is the only place the client-chosen Kerberos realm is ever inspected — after authentication the principal always carries the server realm, so nothing downstream can tell one client realm from another. A header that SpnegoAuthenticator#getBasicRealm cannot read is therefore a header the allow list cannot govern.

That is precisely how the bug fixed in #3251 worked: getBasicRealm split the scheme from the token on a literal space, while SpnegoProvider#parseAuthHeader matches the scheme case-insensitively at offset 0 and then skips a run of any whitespace, possibly empty. So Basic<tab><token> and Basic<token> were authenticated by the library while the allow-list check saw no realm at all — a complete bypass.

The fix works by mirroring the library's parser inside Fess. That means the two parsers can silently drift apart again the next time the library is upgraded. The existing SpnegoAuthenticatorTest#test_getBasicRealm_separatorMatchesLibraryParsing hardcodes the expected realms: it pins Fess's behaviour but never executes the library, so a change to parseAuthHeader or to the library's base64 decoder would leave both CIs green while the bypass returned.

What the test does

SpnegoBasicRealmLibraryParityTest drives the real library parser over 20 Authorization header shapes and compares what the library would authenticate against what Fess extracts:

  • separator variants — Basic <t>, Basic\t<t>, Basic<t>, basic <t>, BASIC<t>, Basic\f<t>, Basic \n<t>, Basic <t>
  • token/credential variants — whitespace inside the token, a CORP\ NetBIOS prefix, a second @ in the user name, no colon, no realm, an empty realm
  • schemes that authenticate nothing — bare Basic, Basic , Negotiate <t>, Bearer <t>
  • a truncated (still decodable) token and a token that is not base64 at all

The realm the library would use is not hardcoded either: the token is decoded exactly as org.codelibs.spnego.SpnegoAuthenticator#doBasicAuth decodes it, and the realm is then derived by javax.security.auth.kerberos.KerberosPrincipal, i.e. by the JDK's own Kerberos name parsing.

Fess's side is called through the production method (getBasicRealm is protected static, and the test lives in the same package). Nothing is copied out of it — copying the body would recreate exactly the tautology this test exists to kill.

Why fail-closed, not equality

The assertion is deliberately one-directional, and there is a class-level javadoc note asking future readers not to "tighten" it into equality.

For every shape where the library would authenticate a credential naming realm R, Fess must resolve a realm at least as narrow as R:

  • never null — that means no check runs at all, which is the original bypass;
  • never a value unrelated to R — that could match some other allow-list entry.

A value that merely ends with "@" + R is accepted. An allow-list entry is a Kerberos realm name and contains no @, so such a value can only ever fail a list that R itself would pass — it over-rejects, which is safe. Over-restriction is always safe here; under-restriction is the vulnerability. Writing the contract this way also keeps the test independent of whether the user name is split at the first or the last @, which is a separate concern.

The test also guards against becoming vacuous: it asserts up front that the canonical Basic <token> header still resolves through the library, so a harness that stopped reaching the parser fails instead of silently skipping every shape.

Reflection note

SpnegoProvider.getAuthScheme(String) is public static, but the SpnegoAuthScheme it returns is a package-private final class and its isBasicScheme() / getToken() are package private, so the test reaches them with getDeclaredMethod(...) + setAccessible(true). Both the library and Fess load from the class path (the unnamed module), so this works on JDK 21 with no --add-opens.

Verification

Test-only change — git diff master --stat shows the single new file; no production code is touched.

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Whole package green (-Dtest='Spnego*Test'): Tests run: 32, Failures: 0, Errors: 0, Skipped: 0.

Proved non-vacuous by temporarily reintroducing the pre-#3251 literal-space split locally (reverted before committing):

AssertionFailedError: Basic\t<token>: the library authenticates realm PARTNER.EXAMPLE,
but the allow-list check reads no realm at all, so spnego.allowed.realms cannot
govern this header ==> expected: not <null>

… parser

On the Basic path, the spnego.allowed.realms check is the only place the
client-chosen Kerberos realm is inspected: after authentication the principal
always carries the server realm. A header that SpnegoAuthenticator#getBasicRealm
cannot read is therefore a header the allow list cannot govern, which is exactly
how the previous literal-space scheme split let "Basic<tab>token" and
"Basictoken" through untouched.

The fix works by mirroring SpnegoProvider#parseAuthHeader inside Fess, so the two
parsers can drift apart again on the next library upgrade. The existing tests
hardcode the expected realms and never execute the library, so such a drift would
leave every suite green while the bypass returned.

This adds SpnegoBasicRealmLibraryParityTest, which drives the real library parser
over 20 Authorization header shapes (separator variants, NetBIOS prefix, extra
'@', missing colon, missing realm, non-Basic schemes, malformed base64) and
compares what the library would authenticate against what Fess extracts.

The assertion is deliberately one-directional rather than an equality check: for
every shape where the library would authenticate a credential naming realm R,
Fess must resolve a realm at least as narrow as R - never null (no check runs at
all) and never a value unrelated to R. Over-rejecting is safe, so a value ending
in "@" + R is accepted; allow-list entries are realm names and contain no '@', so
such a value can only fail a list that R would pass. This also keeps the test
independent of whether the user name is split at the first or the last '@'.

SpnegoProvider#getAuthScheme is public, but the SpnegoAuthScheme it returns is a
package-private final class with package-private isBasicScheme()/getToken(), so
the test reaches them by reflection. Both jars load from the class path (unnamed
module), so setAccessible(true) succeeds with no --add-opens.

Test-only change; no production code is touched. Verified to fail when the
pre-fix literal-space split is reintroduced locally.
@marevol marevol added this to the 15.9.0 milestone Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant