test(sso/spnego): pin the Basic realm check against the library's own parser - #3264
Open
marevol wants to merge 1 commit into
Open
test(sso/spnego): pin the Basic realm check against the library's own parser#3264marevol wants to merge 1 commit into
marevol wants to merge 1 commit into
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this guards
On the Basic path,
spnego.allowed.realmsis 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 thatSpnegoAuthenticator#getBasicRealmcannot read is therefore a header the allow list cannot govern.That is precisely how the bug fixed in #3251 worked:
getBasicRealmsplit the scheme from the token on a literal space, whileSpnegoProvider#parseAuthHeadermatches the scheme case-insensitively at offset 0 and then skips a run of any whitespace, possibly empty. SoBasic<tab><token>andBasic<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_separatorMatchesLibraryParsinghardcodes the expected realms: it pins Fess's behaviour but never executes the library, so a change toparseAuthHeaderor to the library's base64 decoder would leave both CIs green while the bypass returned.What the test does
SpnegoBasicRealmLibraryParityTestdrives the real library parser over 20Authorizationheader shapes and compares what the library would authenticate against what Fess extracts:Basic <t>,Basic\t<t>,Basic<t>,basic <t>,BASIC<t>,Basic\f<t>,Basic \n<t>,Basic <t>CORP\NetBIOS prefix, a second@in the user name, no colon, no realm, an empty realmBasic,Basic,Negotiate <t>,Bearer <t>The realm the library would use is not hardcoded either: the token is decoded exactly as
org.codelibs.spnego.SpnegoAuthenticator#doBasicAuthdecodes it, and the realm is then derived byjavax.security.auth.kerberos.KerberosPrincipal, i.e. by the JDK's own Kerberos name parsing.Fess's side is called through the production method (
getBasicRealmisprotected 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 asR:null— that means no check runs at all, which is the original bypass;R— that could match some other allow-list entry.A value that merely ends with
"@" + Ris accepted. An allow-list entry is a Kerberos realm name and contains no@, so such a value can only ever fail a list thatRitself 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)ispublic static, but theSpnegoAuthSchemeit returns is a package-private final class and itsisBasicScheme()/getToken()are package private, so the test reaches them withgetDeclaredMethod(...)+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 --statshows the single new file; no production code is touched.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):