Skip to content

fix: probe Bearer too, so a half-stripped host stops reading healthy (#121) - #123

Merged
ifahimreza merged 1 commit into
mainfrom
fix/121-probe-bearer-too
Aug 18, 2026
Merged

fix: probe Bearer too, so a half-stripped host stops reading healthy (#121)#123
ifahimreza merged 1 commit into
mainfrom
fix/121-probe-bearer-too

Conversation

@ifahimreza

Copy link
Copy Markdown
Contributor

Closes #121

What

Saddle_Connection::self_check() could report auth_header: ok on a site where every OAuth client was being refused, because probe_headers() sent exactly one loopback request and it carried Basic.

Now it sends two — Basic and Bearer — and reports each.

Why this is the one that mattered

Basic is the scheme that survives where others do not. Apache and LiteSpeed consume an RFC 7617 header natively into PHP_AUTH_USER, so it reaches PHP even on CGI/FastCGI setups that never forward the raw Authorization header, and a firewall rule can be written against Bearer alone.

On such a host:

  • every pasted-key client works — Claude Code, Claude Desktop, Cursor, VS Code, Gemini CLI all send Basic;
  • every app that signs in through Saddle gets 401 — and ChatGPT has no field for a custom header, so Bearer is the only way it can ever connect;
  • and Saddle's own health check said the connection was fine.

From outside that reads as "the AI app is broken", not "my server drops a header". "Claude works against the same endpoint, so the header must be fine" is the wrong inference, and this function was the thing licensing it.

How

  • A second loopback carrying Authorization: Bearer saddle-probe. Neither probe value is a credential; nothing reads them.
  • Judged on the scheme that arrived, not on arrival alone — a host that rewrites an unrecognised Authorization header into something else would otherwise pass a test it should fail.
  • bearer_header: ok|stripped|unknown, and a bearer_header_stripped status ranked below auth_header_stripped (both-stripped is the more severe report) and above the nonce.
  • htaccess_fixable extended to the bearer case. The rule Saddle already generates forwards the whole Authorization header, so it was always the fix for both — it simply was never offered for this one.
  • ConnectionHealth.jsx gets its own copy for the partial case, because the generic warning would be wrong: pasted-key apps are fine, and saying otherwise sends people hunting the wrong thing.

The public probe route gains a field

rest_auth_probe() now returns scheme — its first non-boolean. test_auth_probe_returns_only_the_documented_booleans caught that immediately, which is exactly what it is for; it is updated deliberately rather than loosened. The value is a closed enum ('' / basic / bearer) naming the shape of the credential the caller itself just sent, never any part of its value, and there is a test asserting a token cannot appear in the response.

Testing

  • composer test603 tests (was 599), 2017 assertions, 1 pre-existing skip, green
  • All three new tests verified red without the fix (Undefined array key "bearer_header" / "scheme")
  • The load-bearing one: test_self_check_catches_a_bearer_stripped_while_basic_survives — asserts auth_header: ok and bearer_header: stripped and status: bearer_header_stripped and htaccess_fixable: true, from a loopback stub that answers differently per scheme. That combination is the customer's exact site.
  • Plus the false-positive guard (both schemes survive → ok) and the forward-compat guard (a probe response with no scheme key reads unknown, never stripped — guessing would put a scary, wrong warning on a healthy site).
  • composer lint — 0 errors (pre-existing warnings only); npm run lint:js clean
  • npm run build; languages/saddle.pot regenerated
  • Not yet verified against a real half-stripping host — the loopback is stubbed. The customer round-trip is the live test.

…ing healthy

self_check() could report auth_header: ok on a site where every OAuth
client was being refused, because probe_headers() sent exactly one
loopback and it carried Basic.

Basic is the scheme that survives where others do not. Apache and
LiteSpeed consume an RFC 7617 header natively into PHP_AUTH_USER, so it
reaches PHP even on setups that never forward the raw Authorization
header, and a firewall rule can be written against Bearer alone. On such
a host every pasted-key client works and every app that signs in through
Saddle gets 401 — which reads from outside as "the AI app is broken"
rather than "my server drops a header", and which Saddle's own health
check was confirming.

That matters because the two paths have disjoint client populations.
Application Passwords carry Basic: Claude Code, Claude Desktop, Cursor,
VS Code. OAuth carries Bearer, and ChatGPT has no field for a custom
header, so Bearer is the only way it can ever connect. "Claude works, so
the header is fine" is the wrong inference and this is the code that was
licensing it.

So: a second loopback carrying Bearer, judged on the scheme that arrived
rather than on arrival alone (a host that rewrites an unrecognised
Authorization header would otherwise pass a test it should fail), a
bearer_header field, a bearer_header_stripped status ranked below the
both-stripped case, and htaccess_fixable extended — the rule Saddle
already generates forwards the header whatever scheme it carries, so it
was always the fix for both.

The public probe route gains `scheme`, its first non-boolean. The
contract test pinning its key set caught that, as designed; it is
updated deliberately, and the value is a closed enum naming the shape of
what the caller itself just sent, never any part of it.

Closes #121
@ifahimreza
ifahimreza merged commit 97c733a into main Aug 18, 2026
1 of 7 checks passed
@ifahimreza
ifahimreza deleted the fix/121-probe-bearer-too branch August 18, 2026 03:23
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.

The connection self-check probes Basic only, so a stripped Bearer reads as healthy

1 participant