fix(screenshots): restrict screenshot URLs to our own R2 bucket - #78
Merged
royalpinto007 merged 2 commits intoAug 8, 2026
Merged
Conversation
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.
|
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #39
Submit (
/api/posts) and edit (/api/posts/edit/[token]) both validatedscreenshotUrlswith a plainz.string().url(). The presign flow (app/api/upload/presign/route.ts) carefully restricts content type/size and returns apublicUrlunder our own R2 bucket, but nothing stopped either endpoint from accepting anyhttps://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()inlib/utils/urls.ts— requires the URL to start withgetR2PublicBaseUrl()and the key to look likescreenshots/<uuid>.<ext>(matching whatgetPresignedUploadUrlactually generates). Wired it intoscreenshotUrlsSchema, a shared schema piece now used by both the submit and edit routes, plus the (currently unused, but still exported/typed)screenshotsfield onsubmitSchema.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.tscovers the validator directly (valid key, wrong host, malformed key, unset base URL).app/api/posts/route.test.tsand 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 fullvitestsuite all pass (108/108).