Skip to content

Honeytoken injection was silently off for streaming SSR - #16

Merged
cport1 merged 1 commit into
mainfrom
fix/inject-with-writehead-482
Jul 31, 2026
Merged

Honeytoken injection was silently off for streaming SSR#16
cport1 merged 1 commit into
mainfrom
fix/inject-with-writehead-482

Conversation

@cport1

@cport1 cport1 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #15, found by installing it on a real Angular SSR site.

What was broken

Interception required !res.headersSent. Angular SSR — and Nuxt, and anything else calling res.writeHead() then piping — commits headers before the first byte of body, so that guard disabled injection for exactly the apps most likely to be serving server-rendered HTML.

The failure was silent in the worst way: page rendered correctly, no error logged, nothing injected. A trap that isn't there looks identical to a trap nothing has walked into.

headersSent was the wrong question

What matters is whether a Content-Length has been committed that can no longer be corrected — growing the body past a declared length truncates it at the client. Under Transfer-Encoding: chunked none is declared, so the body is free to change. That's the streaming case.

-  intercepting = !res.headersSent && isInjectableHtml(...);
+  const isHtml = isInjectableHtml(res.getHeader('content-type') as string);
+  const lengthCommitted = res.headersSent && res.getHeader('content-length') !== undefined;
+  intercepting = isHtml && !lengthCommitted;

Found by installing it, not by testing it

All eight existing tests passed. Every one used res.send(), which buffers and sets Content-Length. The suite agreed with itself and was wrong about production.

It took wiring the adapter into a real Angular SSR app and grepping the rendered HTML for the link:

before:  bytes: 60077   honeytoken: (none)
after:   bytes: 60252   honeytoken: /__wd/12016800bdd8

Then following the bait produced the first meaningful SDK detection in this product's history:

{ "source": "sdk_tripwire", "score": 90, "level": "HIGH",
  "signals": [{ "name": "tripwire_triggered", "category": "honeypot", "confidence": 0.95 }] }

Every sdk detection before today scored 0.

Testing

199 green. Two tests added for the paths that were missing: writeHead + chunked injects, and a response with a committed Content-Length is left untouched.

Interception required !res.headersSent. Angular SSR — and Nuxt, and anything
else that calls res.writeHead() then pipes — commits headers before the first
byte of body, so that guard disabled injection for exactly the apps most likely
to be serving server-rendered HTML.

The failure was silent in the worst way: the page rendered correctly, no error
was logged, and nothing was injected. A trap that is not there looks identical
to a trap nothing has walked into.

headersSent was the wrong question. What matters is whether a Content-Length has
been committed that can no longer be corrected — growing the body past a declared
length truncates it at the client. Under chunked encoding none is declared, so
the body is free to change, which is precisely the streaming case.

FOUND BY INSTALLING IT, NOT BY TESTING IT

All eight existing tests passed, because every one of them used res.send(), which
buffers and sets Content-Length. The suite agreed with itself and was wrong about
production. It took wiring the adapter into a real Angular SSR app and grepping
the rendered HTML for the link to see it.

Two tests added for the paths that were missing: writeHead + chunked injects, and
a response with a committed Content-Length is left alone.
@cport1
cport1 merged commit 34fd890 into main Jul 31, 2026
2 checks passed
@cport1
cport1 deleted the fix/inject-with-writehead-482 branch July 31, 2026 00:24
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.

1 participant