HTTP: have getServerHTTPS() honor https baseurlpath when $_SERVER['HTTPS'] is absent#2638
Conversation
…TPS'] is absent
When SimpleSAMLphp is invoked from an external script (e.g. via
\SimpleSAML\Auth\Simple from a host application's controller) behind a
TLS-terminating upstream proxy, $_SERVER['HTTPS'] is absent in PHP --
nginx's stock fastcgi_params ships `fastcgi_param HTTPS $https
if_not_empty;`, and $https is empty when nginx itself receives a plain
HTTP connection from the upstream proxy.
In that topology getServerHTTPS() returns false for what is actually an
HTTPS request, which cascades through five callers -- most visibly via
Session::getSessionFromRequest() throwing CriticalConfigurationError
('Setting secure cookie on plain HTTP') from the secure-cookie check.
The maintainer-prescribed workaround for reverse-proxy deployments
(see PRs simplesamlphp#795, simplesamlphp#376, simplesamlphp#324, simplesamlphp#491) is to set
baseurlpath to a full URL. That works for Utils\HTTP::getBaseURL()'s
full-URL branch but is silently ignored by getServerHTTPS() and its
four downstream callers.
This change has getServerHTTPS() consult the admin-set baseurlpath as a
secondary signal -- but only when:
(a) $_SERVER['HTTPS'] is COMPLETELY ABSENT (preserves explicit 'off')
(b) baseurlpath is a full https:// URL
(c) baseurlpath's host exactly matches the current request's host
(case-insensitive)
No client-controlled header (X-Forwarded-Proto etc.) is read at any
point -- consistent with the maintainer position taken in PRs simplesamlphp#795,
simplesamlphp#376, simplesamlphp#324, and simplesamlphp#491.
Adds 8 PHPUnit tests covering: the bug demonstration, the host-match
guard, the HTTPS=on short-circuit, explicit HTTPS=off preservation,
case-insensitive host comparison, host-with-port in the request,
relative-path baseurlpath rejection, and http:// baseurlpath rejection.
Verified against upstream master: 880 -> 888 tests, 0 regressions.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2638 +/- ##
============================================
+ Coverage 45.80% 45.91% +0.11%
- Complexity 4256 4259 +3
============================================
Files 184 184
Lines 13696 13717 +21
============================================
+ Hits 6273 6298 +25
+ Misses 7423 7419 -4 🚀 New features to boost your workflow:
|
PHPCS reported 8 violations from PR simplesamlphp#2638 — 7x 'Expected 2 blank lines after method, found 1' and 1x 'closing brace for the class must go on the next line after the body'. Auto-fixed by phpcbf. No behavioral change; all 888 unit tests still pass.
`getBaseURL()` (same file) calls `Configuration::getInstance()` without a try/catch; the new `getServerHTTPS()` fallback should follow the same pattern for consistency. The catch's `return false` was untested (would require Configuration to throw, which it doesn't in normal operation) — codecov flagged it as the 1 line of missing patch coverage on PR simplesamlphp#2638.
|
Re: codecov 92.85% patch coverage — the 1 uncovered line was a defensive |
|
LGTM! @thijskh would you mind taking a look before I merge this? |
|
It looks like a valid improvement. Given the various decisions based on different variables, I wonder if adding some debug-level logging would help. So if deployers do not get the URL they expect, they can know what triggered SSP to conclude or not that it is HTTPS. |
|
Makes sense! @esetnik is this something you can add for us, please? |
Per review feedback (@thijskh): when $_SERVER['HTTPS'] is absent and getServerHTTPS() falls back to the baseurlpath check, log a debug-level line explaining which variable drove the HTTPS-or-not conclusion, so deployers who don't get the URL they expect can diagnose it. Three distinct debug lines cover the three fallback outcomes: - baseurlpath host matches current host -> treated as HTTPS - baseurlpath host does NOT match -> not HTTPS (names both hosts) - baseurlpath is not a full https:// URL -> not HTTPS, with a hint to set baseurlpath/application.baseURL for upstream-TLS-terminating deployments Logging is confined to the new fallback branch; the pre-existing $_SERVER['HTTPS'] cases are unchanged and unlogged to avoid noise on the common path. All three lines are exercised by existing tests.
|
Good call — added in
I kept the logging to the new fallback branch only — the pre-existing |
|
@tvdijen already added it in One thing I can't fix from a fork: the PHP-Linter (8.4) job failed on a transient GitHub API rate limit fetching the PHP-Linter on 8.3 and 8.5 passed on the same commit (run 26628316432). Could one of you re-run the failed job when convenient? |
|
The 8.5-ubuntu unit-test failure is also infra, not the diff: Evidence it's unrelated to this change:
Both this and the earlier PHP-Linter (8.4) PHAR rate-limit are transient. A re-run should green the pipeline; I can't trigger it from the fork. Thanks for your patience with the flakes! |
|
It's passing now.. The linters are always failing, so annoying :( |
…TPS'] is absent (#2638) * HTTP: have getServerHTTPS() honor https baseurlpath when $_SERVER['HTTPS'] is absent When SimpleSAMLphp is invoked from an external script (e.g. via \SimpleSAML\Auth\Simple from a host application's controller) behind a TLS-terminating upstream proxy, $_SERVER['HTTPS'] is absent in PHP -- nginx's stock fastcgi_params ships `fastcgi_param HTTPS $https if_not_empty;`, and $https is empty when nginx itself receives a plain HTTP connection from the upstream proxy. In that topology getServerHTTPS() returns false for what is actually an HTTPS request, which cascades through five callers -- most visibly via Session::getSessionFromRequest() throwing CriticalConfigurationError ('Setting secure cookie on plain HTTP') from the secure-cookie check. The maintainer-prescribed workaround for reverse-proxy deployments (see PRs #795, #376, #324, #491) is to set baseurlpath to a full URL. That works for Utils\HTTP::getBaseURL()'s full-URL branch but is silently ignored by getServerHTTPS() and its four downstream callers. This change has getServerHTTPS() consult the admin-set baseurlpath as a secondary signal -- but only when: (a) $_SERVER['HTTPS'] is COMPLETELY ABSENT (preserves explicit 'off') (b) baseurlpath is a full https:// URL (c) baseurlpath's host exactly matches the current request's host (case-insensitive) No client-controlled header (X-Forwarded-Proto etc.) is read at any point -- consistent with the maintainer position taken in PRs #795, #376, #324, and #491. Adds 8 PHPUnit tests covering: the bug demonstration, the host-match guard, the HTTPS=on short-circuit, explicit HTTPS=off preservation, case-insensitive host comparison, host-with-port in the request, relative-path baseurlpath rejection, and http:// baseurlpath rejection. Verified against upstream master: 880 -> 888 tests, 0 regressions. * tests: fix PSR-12 spacing (2 blank lines after methods) PHPCS reported 8 violations from PR #2638 — 7x 'Expected 2 blank lines after method, found 1' and 1x 'closing brace for the class must go on the next line after the body'. Auto-fixed by phpcbf. No behavioral change; all 888 unit tests still pass. * HTTP: drop defensive try/catch around Configuration::getInstance() `getBaseURL()` (same file) calls `Configuration::getInstance()` without a try/catch; the new `getServerHTTPS()` fallback should follow the same pattern for consistency. The catch's `return false` was untested (would require Configuration to throw, which it doesn't in normal operation) — codecov flagged it as the 1 line of missing patch coverage on PR #2638. * HTTP: add debug logging to getServerHTTPS() baseurlpath fallback Per review feedback (@thijskh): when $_SERVER['HTTPS'] is absent and getServerHTTPS() falls back to the baseurlpath check, log a debug-level line explaining which variable drove the HTTPS-or-not conclusion, so deployers who don't get the URL they expect can diagnose it. Three distinct debug lines cover the three fallback outcomes: - baseurlpath host matches current host -> treated as HTTPS - baseurlpath host does NOT match -> not HTTPS (names both hosts) - baseurlpath is not a full https:// URL -> not HTTPS, with a hint to set baseurlpath/application.baseURL for upstream-TLS-terminating deployments Logging is confined to the new fallback branch; the pre-existing $_SERVER['HTTPS'] cases are unchanged and unlogged to avoid noise on the common path. All three lines are exercised by existing tests.
…TPS'] is absent (#2638) * HTTP: have getServerHTTPS() honor https baseurlpath when $_SERVER['HTTPS'] is absent When SimpleSAMLphp is invoked from an external script (e.g. via \SimpleSAML\Auth\Simple from a host application's controller) behind a TLS-terminating upstream proxy, $_SERVER['HTTPS'] is absent in PHP -- nginx's stock fastcgi_params ships `fastcgi_param HTTPS $https if_not_empty;`, and $https is empty when nginx itself receives a plain HTTP connection from the upstream proxy. In that topology getServerHTTPS() returns false for what is actually an HTTPS request, which cascades through five callers -- most visibly via Session::getSessionFromRequest() throwing CriticalConfigurationError ('Setting secure cookie on plain HTTP') from the secure-cookie check. The maintainer-prescribed workaround for reverse-proxy deployments (see PRs #795, #376, #324, #491) is to set baseurlpath to a full URL. That works for Utils\HTTP::getBaseURL()'s full-URL branch but is silently ignored by getServerHTTPS() and its four downstream callers. This change has getServerHTTPS() consult the admin-set baseurlpath as a secondary signal -- but only when: (a) $_SERVER['HTTPS'] is COMPLETELY ABSENT (preserves explicit 'off') (b) baseurlpath is a full https:// URL (c) baseurlpath's host exactly matches the current request's host (case-insensitive) No client-controlled header (X-Forwarded-Proto etc.) is read at any point -- consistent with the maintainer position taken in PRs #795, Adds 8 PHPUnit tests covering: the bug demonstration, the host-match guard, the HTTPS=on short-circuit, explicit HTTPS=off preservation, case-insensitive host comparison, host-with-port in the request, relative-path baseurlpath rejection, and http:// baseurlpath rejection. Verified against upstream master: 880 -> 888 tests, 0 regressions. * tests: fix PSR-12 spacing (2 blank lines after methods) PHPCS reported 8 violations from PR #2638 — 7x 'Expected 2 blank lines after method, found 1' and 1x 'closing brace for the class must go on the next line after the body'. Auto-fixed by phpcbf. No behavioral change; all 888 unit tests still pass. * HTTP: drop defensive try/catch around Configuration::getInstance() `getBaseURL()` (same file) calls `Configuration::getInstance()` without a try/catch; the new `getServerHTTPS()` fallback should follow the same pattern for consistency. The catch's `return false` was untested (would require Configuration to throw, which it doesn't in normal operation) — codecov flagged it as the 1 line of missing patch coverage on PR #2638. * HTTP: add debug logging to getServerHTTPS() baseurlpath fallback Per review feedback (@thijskh): when $_SERVER['HTTPS'] is absent and getServerHTTPS() falls back to the baseurlpath check, log a debug-level line explaining which variable drove the HTTPS-or-not conclusion, so deployers who don't get the URL they expect can diagnose it. Three distinct debug lines cover the three fallback outcomes: - baseurlpath host matches current host -> treated as HTTPS - baseurlpath host does NOT match -> not HTTPS (names both hosts) - baseurlpath is not a full https:// URL -> not HTTPS, with a hint to set baseurlpath/application.baseURL for upstream-TLS-terminating deployments Logging is confined to the new fallback branch; the pre-existing $_SERVER['HTTPS'] cases are unchanged and unlogged to avoid noise on the common path. All three lines are exercised by existing tests.
…TPS'] is absent (#2638) * HTTP: have getServerHTTPS() honor https baseurlpath when $_SERVER['HTTPS'] is absent When SimpleSAMLphp is invoked from an external script (e.g. via \SimpleSAML\Auth\Simple from a host application's controller) behind a TLS-terminating upstream proxy, $_SERVER['HTTPS'] is absent in PHP -- nginx's stock fastcgi_params ships `fastcgi_param HTTPS $https if_not_empty;`, and $https is empty when nginx itself receives a plain HTTP connection from the upstream proxy. In that topology getServerHTTPS() returns false for what is actually an HTTPS request, which cascades through five callers -- most visibly via Session::getSessionFromRequest() throwing CriticalConfigurationError ('Setting secure cookie on plain HTTP') from the secure-cookie check. The maintainer-prescribed workaround for reverse-proxy deployments (see PRs #795, #376, #324, #491) is to set baseurlpath to a full URL. That works for Utils\HTTP::getBaseURL()'s full-URL branch but is silently ignored by getServerHTTPS() and its four downstream callers. This change has getServerHTTPS() consult the admin-set baseurlpath as a secondary signal -- but only when: (a) $_SERVER['HTTPS'] is COMPLETELY ABSENT (preserves explicit 'off') (b) baseurlpath is a full https:// URL (c) baseurlpath's host exactly matches the current request's host (case-insensitive) No client-controlled header (X-Forwarded-Proto etc.) is read at any point -- consistent with the maintainer position taken in PRs #795, #376, #324, and #491. Adds 8 PHPUnit tests covering: the bug demonstration, the host-match guard, the HTTPS=on short-circuit, explicit HTTPS=off preservation, case-insensitive host comparison, host-with-port in the request, relative-path baseurlpath rejection, and http:// baseurlpath rejection. Verified against upstream master: 880 -> 888 tests, 0 regressions. * tests: fix PSR-12 spacing (2 blank lines after methods) PHPCS reported 8 violations from PR #2638 — 7x 'Expected 2 blank lines after method, found 1' and 1x 'closing brace for the class must go on the next line after the body'. Auto-fixed by phpcbf. No behavioral change; all 888 unit tests still pass. * HTTP: drop defensive try/catch around Configuration::getInstance() `getBaseURL()` (same file) calls `Configuration::getInstance()` without a try/catch; the new `getServerHTTPS()` fallback should follow the same pattern for consistency. The catch's `return false` was untested (would require Configuration to throw, which it doesn't in normal operation) — codecov flagged it as the 1 line of missing patch coverage on PR #2638. * HTTP: add debug logging to getServerHTTPS() baseurlpath fallback Per review feedback (@thijskh): when $_SERVER['HTTPS'] is absent and getServerHTTPS() falls back to the baseurlpath check, log a debug-level line explaining which variable drove the HTTPS-or-not conclusion, so deployers who don't get the URL they expect can diagnose it. Three distinct debug lines cover the three fallback outcomes: - baseurlpath host matches current host -> treated as HTTPS - baseurlpath host does NOT match -> not HTTPS (names both hosts) - baseurlpath is not a full https:// URL -> not HTTPS, with a hint to set baseurlpath/application.baseURL for upstream-TLS-terminating deployments Logging is confined to the new fallback branch; the pre-existing $_SERVER['HTTPS'] cases are unchanged and unlogged to avoid noise on the common path. All three lines are exercised by existing tests.
Make
baseurlpath's HTTPS declaration apply consistently ingetServerHTTPS()Fixes #2637.
Problem
Utils\HTTP::getServerHTTPS()consults only$_SERVER['HTTPS']. In nginx → php-fpm deployments behind a TLS-terminating reverse proxy,$_SERVER['HTTPS']is absent — nginx's stockfastcgi_paramsshipsfastcgi_param HTTPS $https if_not_empty;, and$httpsis empty when nginx itself receives plain HTTP from the upstream proxy. SogetServerHTTPS()returnsfalsefor what is actually an HTTPS request, breaking five downstream call sites:Utils\HTTP::getSelfURL()external-script branch$as = new \SimpleSAML\Auth\Simple(…)from a host application's own controller throwsCriticalConfigurationError: Setting secure cookie on plain HTTP(the most visible failure mode)Auth\Simple::getProcessedURL()http://...return-to URLs even when the original request was HTTPSUtils\HTTP::getServerPort()SERVER_PORTis missingUtils\HTTP::getBaseURL()relative-path branchhttp://instead ofhttps://when baseurlpath is a relative pathThe maintainer-prescribed workaround for reverse-proxy deployments — set
baseurlpathto a fullhttps://URL, restated multiple times in #795, #376, #324, #491 — works forUtils\HTTP::getBaseURL()'s full-URL branch (used by the "normal" branch ofgetSelfURL()for SimpleSAMLphp's own/saml/*URLs) but is silently ignored bygetServerHTTPS()and therefore by the four other callers above.For SPs that use the canonical
Auth\Simplepattern from a host application's own controller, the maintainer's verbal guidance in #2484 is "also setapplication.baseURL." That works, but it's not documented in the config template, the SP documentation, or the error message — users repeatedly burn hours discovering it (a decade of duplicate reports: #77, #343, #654, #808, #879, #2484).Approach
Threads the maintainer-stated philosophy explicitly:
X-Forwarded-*headers. Consistent with the position taken in #795, #376, #324, and #491.baseurlpathis set inconfig.phpby the deployment administrator — this is the same source of truth the maintainers have repeatedly endorsed.$_SERVER['HTTPS']is present (set or'off'), behavior is byte-for-byte unchanged. The new fallback only activates when the key is absent.baseurlpath's declared scheme when its host exactly matches the current request's host (case-insensitive). Prevents a multi-host installation from over-promoting unrelated requests to HTTPS just because one configured vhost happens to declare HTTPS.Changes
src/SimpleSAML/Utils/HTTP.php—getServerHTTPS()Refactored so the original three-case
$_SERVER['HTTPS']interpretation runs only whenarray_key_exists('HTTPS', $_SERVER). When the key is absent, falls through to a new admin-config-based check:tests/src/SimpleSAML/Utils/HTTPTest.php— 8 new test methodstestGetServerHTTPSHonorsHttpsBaseurlpathBehindProxytestGetServerHTTPSBaseurlpathHostMismatchDoesNotForceHTTPStestGetServerHTTPSReturnsTrueWhenHttpsEnvIsSettestGetServerHTTPSRespectsExplicitHttpsOffHTTPS=offis not overridden by fallbacktestGetServerHTTPSCaseInsensitiveHostMatchtestGetServerHTTPSHostWithPortInRequestHTTP_HOSTwith port matches baseurlpath without port (port stripped bygetServerHost())testGetServerHTTPSIgnoresRelativeBaseurlpathtestGetServerHTTPSIgnoresHttpBaseurlpathhttp://baseurlpath is not coerced into HTTPSVerification
getServerHTTPS()callers in the library benefit:getServerPort(),getBaseURL()'s relative-path branch,getSelfURL()'s external-script branch (the bug site), andAuth\Simple::getProcessedURL().Backward compatibility
$_SERVER['HTTPS']='on'$_SERVER['HTTPS']='off'$_SERVER['HTTPS']=''$_SERVER['HTTPS']missing, no baseurlpath / relative baseurlpath$_SERVER['HTTPS']missing, http:// baseurlpath$_SERVER['HTTPS']missing, https:// baseurlpath, different host$_SERVER['HTTPS']missing, https:// baseurlpath, same hostProduction deployments that already set
application.baseURLas a workaround are unaffected —getSelfURL()'s external branch checks that first.Security considerations
The new fallback never reads any client-controlled header. The two inputs are:
baseurlpathfromConfiguration(admin-set inconfig.php).$_SERVER['HTTP_HOST']viagetServerHost()(already used by the library;getServerHost()strips any port and falls back toSERVER_NAME/'localhost').There is no widening of the trust surface; the change is strictly within the library's existing trust model.
Reproducer
A self-contained Docker Compose reproducer is attached to the linked issue. Tear-down command included.