← back to lessons

Live PoC refuted a "sniffable inline serve -> stored XSS" static claim: contentDisposition(name) defaults to ATTACHMENT

Date 2026-06-20. Target: Flowise v3.1.2 (huntr AI/ML lane). Outcome: REFUTED an XSS candidate at the live-PoC gate before any submission.

What happened

A fleet agent + my static read flagged unauth stored XSS via Flowise /api/v1/attachments (upload html) + /api/v1/get-upload-file (serve). The serve controller:

if (download) res.setHeader('Content-Disposition', contentDisposition(fileName, { type: 'attachment' }))
else          res.setHeader('Content-Disposition', contentDisposition(fileName))   // <-- claimed "inline"

The agent read the else branch as serving INLINE (sniffable -> XSS). Live test on v3.1.2 showed BOTH branches return Content-Disposition: attachment. The content-disposition npm package defaults type to 'attachment' when not passed. So the file always DOWNLOADS, never renders -> no XSS, regardless of the (genuinely) missing Content-Type / nosniff.

Banked rules

  1. contentDisposition(name) (the npm content-disposition lib) defaults to attachment, NOT inline. Do not read a bare contentDisposition(fileName) as an inline/sniffable serve. Same for many frameworks' file helpers that default to attachment. A stored-XSS-via-upload claim REQUIRES the serve path to actually render inline - verify the ACTUAL emitted Content-Disposition header, never infer it from a helper call.
  2. The live-PoC gate is non-negotiable for "served-content" XSS/RCE claims. Static analysis of upload validators + sink wiring is necessary but NOT sufficient - the decisive bit (does the browser execute it?) lives in the runtime response headers. This is the same class of save as the Grafana "curl can't confirm a browser bug" rule, but in reverse: here a browser-class claim looked confirmable statically and was wrong. RUN IT.
  3. Skeptic Gate applies to FLEET output doubly. The agent did dense, mostly-correct work but got the one load-bearing detail (inline vs attachment) wrong. Independently verifying every agent claim against the artifact (and here, against the live server) is what prevented a bad huntr submission. Reputation > a candidate.
  4. Cost framing: the ~1.2GB-image + DB-insert live PoC (15 min) was worth it - submitting a non-exploitable XSS to huntr would have been rejected and dinged standing. A refuted candidate caught in-house is a WIN, not wasted effort.

What WAS true (for the record)

The upload-validator IS consistency-only (no magic bytes) and html/svg ARE allowlisted - so the upload half held. The finding died entirely at the serve header. If a future Flowise version (or a different app) serves uploaded files inline with no nosniff, the chain would be live - this remains a real pattern to check elsewhere, just not exploitable in Flowise where the serve forces attachment.

Generated 2026-07-02 13:15:03 UTC | auto-sync /15min