Skip to content

fix(screenshots): restrict screenshot URLs to our own R2 bucket - #78

Merged
royalpinto007 merged 2 commits into
AgentPostmortem:mainfrom
waterlemonnn:fix/screenshot-r2-domain-validation
Aug 8, 2026
Merged

fix(screenshots): restrict screenshot URLs to our own R2 bucket#78
royalpinto007 merged 2 commits into
AgentPostmortem:mainfrom
waterlemonnn:fix/screenshot-r2-domain-validation

Conversation

@waterlemonnn

Copy link
Copy Markdown
Contributor

Closes #39

Submit (/api/posts) and edit (/api/posts/edit/[token]) both validated screenshotUrls with a plain z.string().url(). The presign flow (app/api/upload/presign/route.ts) carefully restricts content type/size and returns a publicUrl under our own R2 bucket, but nothing stopped either endpoint from accepting any https:// URL in that field, off-site host included. Those URLs get rendered on public case pages, so it was a way to embed arbitrary third-party content under our domain and leak visitor traffic to it.

Added isOwnedScreenshotUrl() in lib/utils/urls.ts — requires the URL to start with getR2PublicBaseUrl() and the key to look like screenshots/<uuid>.<ext> (matching what getPresignedUploadUrl actually generates). Wired it into screenshotUrlsSchema, a shared schema piece now used by both the submit and edit routes, plus the (currently unused, but still exported/typed) screenshots field on submitSchema.

Left existing rows alone — no rows are known to violate this today since it's newly enforced, and a cleanup script isn't warranted unless a scan turns up violations.

Tests: lib/utils/urls.test.ts covers the validator directly (valid key, wrong host, malformed key, unset base URL). app/api/posts/route.test.ts and the edit route's test file cover the same cases end-to-end through both endpoints (valid URL persisted, external URL 400, malformed key 400, missing/empty screenshots still accepted).

npm run lint, tsc --noEmit, and the full vitest suite all pass (108/108).

waterlemonnn and others added 2 commits August 8, 2026 09:06
Submit and edit both validated screenshotUrls with a plain z.string().url(),
so any https:// URL was accepted and rendered on public case pages. Add
isOwnedScreenshotUrl() to check the URL sits under our R2 public base URL
with a screenshots/<uuid>.<ext> key, and wire it into both routes plus the
unused screenshots field on submitSchema.
@royalpinto007
royalpinto007 merged commit 767123d into AgentPostmortem:main Aug 8, 2026
5 checks passed
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Merged, @waterlemonnn. Thank you for taking the time.

Good, focused change. Exactly the kind of thing agentpostmortem needs.

If agentpostmortem is useful to you, a star goes a long way.

Happy to see more from you whenever you have the time.

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.

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

2 participants