Three pre-existing gRPC auth interceptors accept a whitespace-only configured key as SET (IsNullOrEmpty vs IsNullOrWhiteSpace) #543
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The three pre-existing gRPC auth interceptors —
TelemetryStreamAuthInterceptor,ConfigServeAuthInterceptor,LocalDbSyncAuthInterceptor(all insrc/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/) — guard against an unset configured keywith
string.IsNullOrEmpty(expected). A whitespace-only configured key (e.g." ") is thereforetreated as set, and the interceptor proceeds to
FixedTimeEquals(presented, expected)— so awhitespace-only configured key opens the endpoint to a whitespace-only bearer token, rather than
failing closed the way an actually-unset key does.
Code references (as of
feat/grpc-phase0-contract-auth, commitc2bfad4a):TelemetryStreamAuthInterceptor.cs:120ConfigServeAuthInterceptor.cs:113LocalDbSyncAuthInterceptor.cs:118The two new mesh-transport interceptors added in the ClusterClient→gRPC migration's Phase 0
(
MeshCommandAuthInterceptor, node-side, andCentralCommandAuthInterceptor, central-side) usestring.IsNullOrWhiteSpacefor the equivalent check instead, closing this specific gap in the newcode. This issue tracks whether the same hardening should be backported to the three existing,
shipped, live-gated endpoints.
Why this is an issue and not a drive-by fix
Backporting
IsNullOrWhiteSpaceto the three existing interceptors is a behaviour change onendpoints that are already shipped and live-gated (telemetry stream, config-serve fetch-and-cache,
LocalDb sync) — a deployment currently relying on a whitespace-only key (however unlikely) would
start failing closed instead of accepting a whitespace bearer. That warrants its own decision and
its own live gate, rather than a silent drive-by change bundled into the gRPC migration's Phase 0.
Suggested scope
Decide whether to align all three (and any future auth interceptors) on
string.IsNullOrWhiteSpace, then live-gate each affected endpoint (telemetry stream, configfetch, LocalDb sync) to confirm a real whitespace-only-key deployment (if any exists) does not
silently break, before merging.
Fixed, merged to
master@1df84703and pushed (commitf10f4c37, doc follow-up48707852).All three interceptors now guard with
string.IsNullOrWhiteSpace(Telemetry→PermissionDenied, ConfigServe→Unauthenticated, LocalDbSync→PermissionDenied — status codes and message text unchanged), matching the two mesh interceptors. Red-first verified: with the source change stashed, exactly the 3 new_WithBlankKeyConfigured_IsDeniedfacts failed — a configured key" "really was satisfied by bearer" ". Also added the two missingHasExactlyOnePublicConstructorpins (ConfigServe/LocalDbSync were unpinned; Telemetry already had one) and the sibling-standard<remarks>paragraph.On the issue's decision + live-gate asks:
IsNullOrWhiteSpace— done.rg IsNullOrEmptyunder Host/Configuration now returns only one non-auth filesystem-path check. Code review (security lens) confirmed no trim anywhere in the bearer-extraction chain (a trimmed comparison could have reopened the hole) and no new timing signal (guard ordering unchanged;FixedTimeEqualsstill the only byte comparison).telemetry-docker-dev-keyetc.) and production delivers via${secret:...}.master@9420f8ef):Telemetry__ApiKey: " "): whitespace bearer → PermissionDenied ("no API key is configured on this node") — the exact call that pre-fix would have been ACCEPTED; real key against the whitespace-configured node → PermissionDenied (fail-closed, as an unset key behaves). Override reverted; positive probe with the real key then held an open stream (DeadlineExceeded after the 6 s client window, not a denial), and central's dialer recovered with no new failures past its backoff window.Full suite on the merged SHA: exit 0, 50/50 suites, 9,333 passed / 0 failed.