TI-2026-060D โ The Redirect That Told the Truth
Classification: HIGH ยท Mechanism Analysis Confidence: HIGH โ direct sensor evidence, decoded verbatim from the wire TLP:WHITE โ attacker target paths shown in full; the operator's own hostnames are redacted. Series: The 200 That Lied โ Part D. Every prior part fought the mask. This part discovers that the strongest mask in the corpus is also, by its own design, an informant.
The cold open: a scanner asks for a secret, is handed a login page, and leaves satisfied
A scanner probes one of our authentication-protected services for /.env โ the flat file where web applications keep their database passwords, API keys and cloud credentials. It expects one of two answers: the file (jackpot), or a 404 (move on). Instead, a forward-auth proxy โ Authelia โ steps in front of the request and returns something the scanner did not ask for and cannot use: a 200 OK and a login single-page app.
To the scanner's tooling, this reads as "protected, no file here." It moves on. To a status dashboard watching our edge, it reads as 200 โ success, green, a satisfied visitor. Across this series we have called that the mask, and it is the strongest mask we have found: not a catch-all guessing a 200, not an SPA defaulting to one, but a security control deliberately answering every hostile probe with the same success code a real user gets, by design, as its whole job.
And yet. When Authelia intercepted that request, it had to solve a mundane problem: after the user logs in, where do we send them back to? It cannot forget the original destination โ so it writes it down, in a query parameter on the login URL, called rd (redirect). URL-decode that parameter and there it is, verbatim, the thing the 200 concealed:
rd = https://<redacted-host>/.env (the target)
rm = GET (the method)
The proxy that produced the perfect mask also produced a perfect record of what it masked. The scanner asked for a secret, was handed a login page, and walked away certain it had left no trace โ while the security control that stopped it quietly wrote its exact intent into the redirect. This part is about that redirect, and about the paradox at its heart: the best hiding place in the modern web is also the best witness.
The mechanism: how a mask becomes a confession
It is worth being precise about why this works, because the paradox is not luck โ it is structural, and structural things generalise.
A forward-auth proxy sits in front of a protected service. Every request is first shown to the proxy, which asks a single question: is this user authenticated? If yes, the request passes through. If no โ and an unauthenticated scanner is always "no" โ the proxy does not return the resource and does not return a 404. It returns its login interface, so a real user can sign in. That interface is a normal web page: HTTP 200.
But a login page is useless unless it can send the user back to where they were going. So the forward-auth flow embeds the original request in the login URL. In Authelia's case, two parameters carry it:
rdโ the redirect destination: the full original URL, including the path the scanner wanted.rmโ the request method:GET,POST, whatever verb the scanner used.
Our pipeline does the decode automatically: it extracts rd, URL-unescapes it (often double-encoded), splits it into rd_target_host and rd_target_path, and recovers rm as the effective method. The result is that a masked 200 is re-expressed as its effective_status (401 โ unauthorised) and annotated with the exact resource the attacker was after. The status told us nothing; the redirect told us everything.
This is the general principle the whole series orbits, stated at its sharpest: the features that make a system convenient are the same features that make an attack against it invisible โ except where the convenience must, mechanically, keep a record. A catch-all 200 keeps no record and is pure mask. An auth-proxy 200 must keep the redirect, and so the mask leaks. Find the place where the convenience is forced to remember, and the camouflage becomes evidence.
What the redirect said: a .env dictionary attack, in full
Decode the rd parameter across the corpus and a campaign resolves out of the wall of 200s that no status dashboard could ever have seen. The dominant target is not a page. It is a secret, guessed by dictionary:
Decoded ?rd= target | Hits | What it is |
|---|---|---|
/.env | 35 | the primary prize โ app/cloud secrets |
/.env.local ยท /.env.development | 14 ยท 14 | local & dev deployment secrets |
/.env.production ยท /.env.prod | 13 ยท 9 | production secrets |
/.env.test ยท /.env.staging | 12 ยท 10 | pre-prod secrets |
/.env.dist ยท /.env.sample ยท /.env.example | 10 ยท 10 ยท 9 | template files devs forget to delete |
/.env.save ยท /.env.backup ยท /.env.bak | 10 ยท 9 ยท 9 | careless backups โ the softest hit |
/api/.env ยท /.env.dev | 9 ยท 9 | nested & alt-named variants |
/.git/config | 17 | source-repo config โ full code + secrets |
/wp-includes/id3/license.txt | 80 | a WordPress version tell (fingerprinting) |
Count them: more than fifteen distinct .env filename variants. This is not a scanner that tries /.env once and gives up. It is a dictionary โ a curated list that anticipates every deployment convention (production, staging, local), every framework default (.env.dist, .env.example), and every human error (.env.bak, .env.save, the file you copied "just in case" before an edit). Each variant is a hypothesis about how this victim might have been careless, and the attacker tests all of them. /.env.bak and /.env.save are the tell of a professional: they are not guessing that the secret is exposed; they are guessing that a backup of the secret is exposed, which is the mistake even careful teams make.
Every single one of these requests returned 200. Every single one of them is, on a status dashboard, a satisfied visitor. And every single one of them was recorded, filename-exact, in a redirect parameter the attacker never imagined anyone would read. This is the .env-harvesting industry โ the one documented in this corpus as The .env Harvesters (046A) and The Keys Left in the Door (059E) โ operating at full volume behind an auth proxy, measurable only because the proxy confessed.
This is not "Scanners in the Mirror"
A companion dossier, TI-2026-059D โ Scanners in the Mirror, looked at the auth proxy too โ but at a different problem: how to triage the scanner-fingerprint bucket, separating real hostile scanners from crawler-impersonators from legitimate search bots that all get the same login 200. That is the classification question โ who is knocking.
Part D is the forensic question โ what they asked for, read verbatim from the mechanism. 059D taught you not to blanket-ban the bucket; 060D shows you that inside the bucket is a decoded, filename-exact confession of intent, and that the confession exists because the auth flow is architecturally incapable of not writing it down. The two are complements: one tells you which knocks to take seriously, the other tells you exactly what each serious knock was reaching for.
The seven questions
Who is this? The .env-harvesting cohort โ the same broad population documented across 046A/059E โ now observed operating behind our auth proxy, its intent legible only through the decoded redirect.
Why did it attack? Credential theft. A .env file is a single flat file that can contain a database password, a cloud IAM key, an SMTP login and a third-party API secret all at once. It is the highest-value-per-byte target on the web, and the dictionary of variants is a machine for finding it wherever a developer left it.
Who does it claim to be? It makes no claim โ it relies on the proxy's 200 to make the claim for it, to the defender. The mask is not the attacker's costume; it is the defender's own instrument, borrowed against them.
Whom is it related to? The .env census ties directly to the credential-harvest campaigns of the wider corpus (046A, 059E) and to the same probe-intent (credential_harvest) that lights up across the honeypot and web layers. The redirect is one more lens on an actor set the entity graph already tracks.
Where is the money? A single leaked production .env is worth more than a thousand webshells: it is instant, authenticated access to the victim's cloud account, database and mail โ sold directly or drained directly. The dictionary is cheap; the payout is a company.
What is the official anchor? The auth proxy itself is the anchor of this finding โ a legitimate, widely-deployed open-source security control (Authelia and every forward-auth proxy like it) whose correct, standards-compliant behaviour produces both the mask and the record. There is no misconfiguration here to blame; the paradox is in the design.
What if it is not who it claims / what is hidden? See below.
What ifโฆ
- What if the
?rd=targets are just users mistyping URLs? Users do not mistype/.env.bak,/.env.saveand/.git/configin sequence. A human fat-fingers a real page; a dictionary enumerates backup extensions. The variant structure is machine-generated and self-evidently so. - What if decoding
?rd=invents intent that isn't there? Therdvalue is the attacker's own request URL, preserved byte-for-byte by the proxy. We are not inferring intent; we are reading the request the attacker literally sent, recovered from where the proxy stored it. It is as direct as evidence gets. - What if the
200s mean the proxy is working, so there is nothing to worry about? The proxy is working โ it blocked every one of these. The worry is not that the attacks succeeded here; it is that on the millions of hosts without a decode-the-redirect pipeline, this exact campaign is running invisibly, its200s filed as clean, its.envdictionary unmeasured. Our proxy blocked it; our decode counted it. The internet at large does neither.
Steelman: the strongest case that the redirect is a distraction
"You are congratulating yourselves for reading a parameter your own proxy generates. The ?rd= is an artefact of your specific stack; most of the web does not use forward-auth, so this 'confession' is a curiosity, not a general method. And a blocked .env probe is a non-event โ the file wasn't there, nobody got owned. You have written a dossier about an attack that failed, decoded from a field only you have."
It is the fairest challenge in the series, and it is wrong in a way that matters:
- The specific parameter is ours; the principle is universal.
?rd=is Authelia's spelling, but every forward-auth system (oauth2-proxy, Vouch, Cloudflare Access, Azure AD App Proxy, nginxauth_request) must preserve the original destination to redirect after login. The field name changes; the mechanical necessity does not. The method โ find where the convenience is forced to keep a record, and decode it โ generalises to every one of them. - A blocked attack is the only kind you can safely study. We can publish this filename census precisely because it failed here โ no victim, no harm, pure measurement. The value is not "we stopped one scanner"; it is "we can now quantify a campaign that is, by construction, invisible to the status-based world, and hand defenders the decoded dictionary to hunt for it on hosts that are not protected."
- "The file wasn't there" is survivorship bias. The census tells you what the attacker believed might be there โ the full menu of developer mistakes they came prepared to exploit. That menu is the intelligence. It tells a defender exactly which fifteen filenames to make sure are not reachable on their own estate, before someone without an auth proxy finds out the hard way.
The steelman is right that we decoded a field peculiar to our stack. It is wrong that this makes the finding peculiar. Confidence: HIGH.
Read between the lines
There is a strange justice in the mechanism, and it is the emotional core of this whole series. Three times now the villain has been a success code standing where a warning should be โ the SSRF 200 of Part A, the catch-all 200 of Part B, the WebDAV 207 of Part C. Each was the web's convenience turned into the attacker's camouflage. The .env scanner arrived expecting the same deal: hide inside a 200, leave no trace.
But the auth proxy is a different kind of 200. It is the only mask in the series that cannot afford to forget, because forgetting would break its actual job โ sending a real user home after login. And so, uniquely, its convenience does not merely camouflage the attack; it records it. The attacker exploited the defender's instrument to become invisible, and the very same instrument, doing the very same job, wrote their intent into a parameter and handed it to us. They hid inside the one 200 that keeps a diary.
That is the lesson to take from Part D and carry to the rest of the web: do not ask a system what status it returned. Ask it what it had to remember to do its job โ and read that. Convenience that must keep state is convenience that must keep evidence.
The human layer
Behind /.env.production is a specific, ordinary catastrophe. Somewhere a developer, under deadline, committed a production secrets file to a web-reachable directory, or left a .env.bak after a late-night fix, and moved on. They will never know a machine guessed all fifteen names for that file, because on their server โ with no auth proxy, no decode, no sensor โ every guess returned a 200 or a 404 that told them nothing. The first they will hear of it is a cloud bill for someone else's crypto-mining, or a ransom note, or a customer database for sale. The redirect we decoded is, in the end, a list of the exact mistakes that end companies โ written down by the only witness in the room, on a case where, this time, no one got hurt. The census is a warning we can publish because it is, for once, a near-miss instead of an obituary.
Defense โ decode the redirect, and hunt the dictionary
Two controls fall out of Part D. (1) Decode, do not trust. A 200 from a forward-auth proxy is a 401 with a payload; extract the rd/rm (or your proxy's equivalent), recover effective_status and the true target, and score that. Any stack running forward-auth already has this evidence โ it is one URL-decode away from being intelligence instead of noise. (2) Hunt your own dictionary. The decoded census is a wordlist: /.env and its fifteen variants, /.git/config, the framework tells. Turn it around โ make sure not one of those paths is reachable on your estate, on any host, auth-proxied or not. The attacker did the work of enumerating every way a secret leaks; the redirect handed you their list; the only remaining task is to be certain the list comes back empty.
Investigation metadata
| Field | Value |
|---|---|
| Dossier | TI-2026-060D โ The 200 That Lied (Part D) |
| Date | 2026-07-05 |
| Primary sensor | LSN web-threats layer โ Authelia forward-auth interception, ?rd=/rm= decode |
| Key evidence | 9,670 masked requests (majority mask_source=authelia); decoded .env dictionary (15+ variants) + /.git/config + WP version tells |
| Corroboration | Cross-corpus credential-harvest series (TI-2026-046A The .env Harvesters; 059E The Keys Left in the Door); companion 059D (scanner triage) |
| Mechanism | Forward-auth 200 login SPA preserves original target in rd; decode recovers effective_status=401 + verbatim path |
| Confidence | HIGH (decoded verbatim from the wire; mechanism is standards-mandated, not incidental) |
Next in the series โ TI-2026-060E: The Unified Field โ the synthesis. Four axes (status, host, method, redirect), one actor at a time: how cross-axis convergence turns four weak, individually-deniable signals into a single attribution the entity graph can name.
Every other mask in this series forgot the attack the moment it hid it. The auth proxy could not afford to forget โ so it hid the attack and, in the same motion, wrote it down. The scanner trusted the 200 to keep its secret. The 200 kept a diary instead.