Skip to content

Fix horizontal authorization bypass on /download (IDOR) - #11

Open
thistehneisen wants to merge 1 commit into
cert-lv:masterfrom
thistehneisen:fix/download-authz
Open

Fix horizontal authorization bypass on /download (IDOR)#11
thistehneisen wants to merge 1 commit into
cert-lv:masterfrom
thistehneisen:fix/download-authz

Conversation

@thistehneisen

Copy link
Copy Markdown

Fix horizontal authorization bypass on /download (IDOR)

Reported by: OffSeq — Nils Putniņš <npu@offseq.com>
Affected version: v2.6.1 (a2a794ef6916d16da3fcc2f9f8d86225482e7263)

What this PR fixes

downloadHandler previously verified only:

  1. That the requester held a valid session.
  2. That the requested filename did not contain .. or /.

It then called http.ServeFile(w, r, config.Upload.Path+"/processed/"+file)
for any matching file. There was no check that the requester actually
owned that file.

Filenames in upload/processed/ follow the deterministic pattern
<timestamp>-<original-name>, where the timestamp comes from a
1-second-resolution clock (upload.go:141). Any signed-in user who can
guess (or simply enumerate) the timestamped name of another user's
processed report can fetch the full JSON / table output:

# attacker (account B) downloads victim's (account A) processed report
curl -sk -b attacker.jar \
   "https://HOST:8443/download?file=20260514-102943%2B03-victim-secret.txt" \
   -o stolen.bin
# -> HTTP 200, body is the victim's full graph + indicator data

The reports contain the user's full uploaded indicator list and the
results of querying every backend data source — sensitive material for
a threat-intel platform.

The fix

  • Tighten the filename filter: reject empty string, \\, and \x00 on
    top of the existing .. / / check.
  • Look up the requester's account and verify the requested filename
    appears in their own Uploads.Out list before serving.
  • Return 404 Not Found (with a server-side ERR log) for both rejected
    filenames and cross-user attempts. The previous code returned an empty
    response with no status, which silently obscured the bypass.

Single file changed, +32 / -1 lines.

Repro

Pre-patch

[*] Creating victim and attacker accounts
[*] Victim uploads a sensitive indicator list
[*] Attacker downloads victim's file via /download
    HTTP 200, size      834
[+] EXPLOIT OK: attacker read victim's processed report (no auth check on /download)
    Report for the uploaded file: victim-runXXXXXXXXXX.txt
    Indicator: name='John'
    [{"edge":{"label":"lives in"},"from":{"attributes":{"age":"25", ...

Post-patch

Same script (pocs/03_idor_download.sh):

[*] Attacker downloads victim's file via /download
    HTTP 404, size       19
[-] Could not retrieve victim's report

Server log entry:

ERR Unauthorized download attempt: file not in user's downloads list
    ip=127.0.0.1 username=attackerXXX_b filename=20260514-102943+03-victim-...

Positive control (owner downloading their own file) still works:

[*] Victim downloads OWN file...
    HTTP 200, size      834
    Report for the uploaded file: victim-runXXXXXXXXXX.txt

Notes

  • db.getAccount(username) is called once per /download request. The
    same cost is paid by indexHandler and other authenticated handlers,
    and the Mongo collection is small / indexed, so the overhead is
    negligible. If a future hot path matters, the online[] map already
    caches the account for users with an active WebSocket and could be
    consulted first.
  • account.Uploads.Out is currently scanned linearly. Lists are bounded
    by upload.deleteInterval/upload.deleteExpiration so this is fine in
    practice; a map[string]struct{} could replace it later if profiles
    grow.

Nils Putniņš / npu[at]offseq[dot]com / OffSeq
https://offseq.com / https://radar.offseq.com

`downloadHandler` previously only checked that the requester held a valid
session and that the requested filename did not contain ".." or "/".
It then `http.ServeFile`'d any matching file under upload/processed/.
That let any signed-in user read any other user's processed report by
asking for the (predictable, timestamp-prefixed) filename — a horizontal
authorization bypass affecting the confidentiality of every user's
indicator lists and query results.

This patch:

* Tightens the filename filter (rejects "", "\\", and NUL in addition
  to ".." and "/").
* Looks up the requester's account and verifies the requested filename
  appears in their own Uploads.Out list before serving.
* Returns 404 (with a server-side log) for both rejected filenames and
  cross-user attempts, instead of the previous silent empty response.

Verified:
  * Attacker (User B) requesting User A's processed file → HTTP 404,
    server log "Unauthorized download attempt".
  * Owner (User A) requesting their own file → HTTP 200, 834-byte report,
    unchanged behaviour.
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