I never wrote a single line of code that logged anyone's National Insurance Number. That's what made this one uncomfortable to find.
GovBridge sits in front of several UK government APIs, and some of those endpoints take genuinely sensitive identifiers as part of the request - a NINO for an Income Tax lookup, a VAT registration number, a Unique Taxpayer Reference. Reasonable enough. That's what the underlying government APIs actually require.
The problem wasn't the request. It was everything that happened after the request, without me ever asking it to.
Where it was actually leaking
Most of these identifiers weren't sitting quietly in a request body, where sensible logging conventions tend to leave things alone. They were in the URL path -
/individuals/{nino}/income-tax/...
- because that's how the government's own APIs are structured, and a gateway sitting in front of them naturally mirrors that shape.
And request paths get logged. By default. Almost everywhere. Your web server logs them. Your framework's request logging middleware logs them. Any APM or monitoring tool you've wired in logs them. None of that logging was written with "this specific path segment happens to be someone's tax identifier" in mind, because generic logging infrastructure has no way to know that.
So without a single deliberate Log.Info(nino) anywhere in my codebase, a real person's National Insurance Number was sitting in plain text in application logs, purely as a side effect of URL structure.
Why this is so easy to miss
Nobody sets out to log PII. It happens because logging infrastructure and API design are built by different parts of your brain, on different days, and they never actually talk to each other. Your route design is driven by REST conventions and what the upstream API expects. Your logging setup is driven by "I want visibility into what's happening in production." Neither decision, on its own, looks wrong. The collision between them is invisible until you specifically go looking for it.
And this isn't a government-API-specific problem. Any system where a sensitive identifier - an account number, a medical record ID, anything regulators would call out, ends up as a path segment or query parameter has the same exposure, completely independent of what industry you're in.
The fix
The real fix isn't "stop putting sensitive data in URLs" sometimes you don't control that, especially when you're mirroring an upstream API's own routing structure, the way GovBridge does.
The fix is treating log redaction as its own explicit layer, not an afterthought bolted onto whatever logging framework you're using. Concretely, that meant building a dedicated redaction component - a SensitiveDataRedactor - that knows the shape of the identifiers that matter (NINO, VRN, UTR, and anything added to that list going forward) and scrubs them from anything about to be written to a log, regardless of whether they showed up in a path, a query string, or a header.
The important design choice: this lives centrally, in the shared core of the application, not scattered across individual controllers or endpoints. A rule like "don't log this identifier" is exactly the kind of thing that's useless if you have to remember to apply it every time you add a new endpoint. It only works if it's structurally impossible to forget.
Why I'm writing this up
Because I suspect a meaningful number of people reading this have some version of this exact gap sitting in their own systems right now, undetected, for the same reason mine was: it doesn't look like a bug from either side. The routing looks fine. The logging looks fine. It's only wrong when you look at both at once.
If you've got sensitive identifiers anywhere in your URL structure, and if you're integrating with any regulated API, you probably do, it's worth a specific, deliberate check: open your actual logs, not your code, and see what's really sitting in them.













