Repository navigation
fix(logging): remove query strings from logged URLs - #16
Merged
Merged
Conversation
Symfony's router logs every matched route at info on the request channel
with the full request URI, query string included. So any query value,
such as an e-mail address in a search parameter, reached the logs. That
contradicts docs/architecture/observability.md ("query strings … are not
logged"). Reproduced on the dev stack: a value sent as
/api/v1/system/status?sprintcheck=… appeared in the api log.
The redactor now replaces the query string (and fragment) of values under
URL keys (request_uri, uri, url, referer, referrer) with "?[redacted]",
as the edge's access log already does. The request channel stays
enabled, since Symfony also logs uncaught exceptions there.
Extend the URL keys whose values lose their query string and fragment with redirect_uri, location, target_path, href and return_to, and test every URL key.
The query sent by ST-40 to ST-43 only had token=, which was masked before query strings were removed, so ST-43 could not detect a logged query string. Also send search=, whose key does not look sensitive, and require its value to be absent from the api and edge logs.
Symfony's RouterListener quotes the full Referer in the message of an unknown route (No route found ... (from "<referer>")), which is logged at info, so the query of the page the user came from (search terms, other personal data) reached the log. Only URL values under known context keys lost their query. scrub() now replaces the query and fragment of every scheme://… URL in messages and strings with ?[redacted].
An unknown route requested with a Referer that carries a search marker must not log the marker. The negative control drops the free-text URL query redaction.
Only the key rule removes the query of a relative URL such as a redirect target; absolute URLs are also masked by the free-text rule, so no test noticed when the key rule was removed. Every URL key is now checked with a relative URL, and a negative control removes the key rule.
ST-43 also requests an unknown route with a Referer whose query holds a marker. The marker must be in neither the api nor the edge log, and both records of that request must quote the Referer as ?[redacted], so the check fails if the request never reached the logs.
The redaction table lists the URL keys whose relative and absolute values lose their query. The negative-control count is 38 with #23 and this branch.
knittingcode
added a commit
that referenced
this pull request
Oct 24, 2026
knittingcode
added a commit
that referenced
this pull request
Oct 24, 2026
knittingcode
added a commit
that referenced
this pull request
Oct 24, 2026
Conflict in docs/architecture/authentication.md: kept this branch's unit test row; the control count is main's 38 plus this branch's 10.
knittingcode
added a commit
that referenced
this pull request
Oct 25, 2026
Negative-control analyses found protections on main whose regressions no test would catch, and protections that were tested but had no control. RouteSecurityTest pins the exact permission of every tenant route, so a weakened or new tenant route must be added on purpose. AuditTrailTest checks that the operator's disable, enable and reset-password are audited with the operator as actor and no secrets, and that enabling a user, creating a user or an organization and adding a membership store nothing when their audit event cannot be written. SecurityLoggingTest checks that the spellings of an address that reach one account share one pseudonym (#23), and JsonLineFormatterTest that a URL key loses its fragment even without a query (#16). Ten new negative controls (53 with main's 43): a tenant route weakened to another permission, enabling a user without an audit event or outside its transaction, URL fragments under URL keys, the referer key, values of sensitive keys, database driver messages, the failed-login pseudonym folding, and the member list order and total. Tests and documentation only; no production code changes. When the membership administration API (#19) is merged after this, its three membership routes must be added to the pinned map (the test fails otherwise) and the documented control count recounted.
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.
Summary
Symfony's router logs every matched route at info on the
requestchannel with the full request URI, query string included. That contradictsdocs/architecture/observability.md("query strings … are not logged"). Found by the overnight security review (OVN-104); reproduced on the dev stack, where a value sent as/api/v1/system/status?sprintcheck=…appeared in the api log.Change
LogRedactorreplaces the query string (and fragment) of values under URL keys (request_uri,uri,url,referer,referrer) with?[redacted], as the edge's access log already does. Therequestchannel stays enabled, because Symfony also logs uncaught exceptions there.Tests
JsonLineFormatterTest: the router record's query value (an e-mail and a page number) is removed; a URL without a query is kept.Limitations
Updates after the audit (2026-10-24)
redirect_uri,location,target_path,hrefandreturn_toare now URL keys too (e5e0082). A unit test checks every URL key.?token=, which was masked even before this PR. It now also sends?search=…, a value the old code logged inrequest_uri(de5873b).requestchannel. A?search=markerin the Referer was confirmed in the log against real PHP-FPM.LogRedactor::scrub()now removes the query and fragment of anyscheme://URL in free text (3e640ee).LoggingTest::testTheQueryOfAReferrerIsNotLoggedand unit cases (e967d86).docs/architecture/observability.md./path?x) are not cleaned. Symfony's 404 message contains the absolute requested URL without its query, plus the Referer.Updates before the merge (2026-10-24)
fa6b09d, merge commit, no conflicts, no force push).c514fbc):testEveryUrlKeyLosesTheQueryOfARelativeUrlchecks every URL key with/accounts?search=…#…. Only the key rule protects such values; before, removing it went unnoticed because absolute test URLs are also masked by the free-text rule. A new negative control, "URL keys keep their query", removes the key rule (11 of 11 tests fail under it).e1aa80a): ST-43 now also requests an unknown route withReferer: https://crm.example.test/accounts?search=<marker>. The marker must be in neither the api log (real PHP-FPM, dev Monolog configuration on stderr) nor the edge access log, and both records of that request must quote the Referer as…/accounts?[redacted], so the check cannot pass when the record is missing.531c132): the redaction table lists the URL key rule; the negative-control count is 38 (it said 35; main had 36 after fix(security): share the login limits across every spelling of an account #23).531c132): unit 145, architecture 23, PHPStan, PHP-CS-Fixer, Deptrac and ShellCheck (-x) clean; in a disposable Compose project,LoggingTest,JsonLineFormatterTestandSecurityLoggingTestOK (66 tests), and both logging controls detected."inside a query (browsers encode it), and PHP errors raised before Symfony's handlers exist, which PHP-FPM writes to stderr unredacted (unchanged by this PR).