Skip to content

Screenshot URLs are not restricted to our own R2 bucket, so the upload restrictions are bypassable #39

Description

@royalpinto007

Problem

Screenshot evidence URLs are validated only as "some URL":

  • lib/schemas/submit.ts line 51: screenshots: z.array(z.string().url()).max(5)
  • app/api/posts/edit/[token]/route.ts: screenshotUrls: z.array(z.string().url()).max(5).optional()

Nothing checks that the URL points at our own R2 bucket. The upload flow (app/api/upload/presign/route.ts plus lib/r2/upload.ts) carefully restricts content type and size and returns a publicUrl built from getR2PublicBaseUrl(), but the submit and edit endpoints happily accept any https:// string in that field instead.

Why it matters

A submitter can point screenshots at any third party host. Those URLs are then rendered on public case pages, which means:

  • arbitrary remote content is embedded under our domain, and it can be swapped after moderation approval, so what a moderator approved is not what visitors later see;
  • every case page view leaks visitor IP and referrer to whatever host was supplied;
  • it is a cheap vector for hosting content we did not screen.

The presign hardening is effectively bypassable because it is not the only way to get a URL into the field.

Suggested approach

  1. Add a shared validator, for example isOwnedScreenshotUrl(url) in lib/utils/urls.ts, that requires the URL to start with getR2PublicBaseUrl() and to have a plausible object key shape (screenshots/<uuid>.<ext>).
  2. Use it in a .refine() on both submitSchema.screenshots and the edit route's screenshotUrls.
  3. Decide and document the behavior for existing rows that violate the rule (probably: leave them, add a follow up cleanup script under scripts/).

Done when

  • Submitting or editing a case with an off site screenshot URL returns 400 with a clear message.
  • Tests cover: valid R2 URL accepted, external URL rejected, malformed key rejected, empty array accepted.

If you want to take this on, comment on the issue to claim it and it will be assigned. Please keep to a maximum of 2 open claims per person at a time so other contributors get a chance.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingclaimedSomeone asked first and is working on thisenhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions