Should I vibe code
Managed API for rendering websites, HTML, and Markdown as images or PDFs
A URL parameter is an instruction to your server to fetch something. Point it at 169.254.169.254 and see.
?
Their verdict, the Basic price and the build-time estimate come from their entry, MIT-licensed. Checked 2026-08-04.
?
Our verdict, the regret score and everything below it. Editorial and unsponsored — nobody can pay to be moved.
The honest answer
why the verdict is what it is
Two lines of Playwright and you have a working screenshot endpoint, which is why this one is so easy to get wrong. Read the endpoint back as a sentence: it accepts a URL from a caller and makes your server fetch it, with a full browser, from inside your own network. That is server-side request forgery offered as a product feature. Your cloud's metadata endpoint is one hop away and hands out IAM credentials to anything that asks from inside the box; so is the admin panel on 10.0.x that has no auth because it is "internal". An allowlist does not save you, because a redirect and a DNS answer that changes between your check and Chrome's connection both walk straight through it. Then there is the second life your endpoint has: once it is public and unmetered it is a free rendering proxy, and whatever it gets pointed at — phishing pages, scrapes, things you would rather not have in a bucket — arrives from your IP address and lands on your bill.
What actually breaks
not "if". the specific failures.
- The URL parameter, on day one: link-local metadata at 169.254.169.254, localhost, 10.x, 192.168.x, file:// and gopher:// all render happily unless you blocked them by hand
- Your allowlist, defeated by a 302 — you validated the hostname, Chrome followed the redirect somewhere else, and the check never ran again
- DNS rebinding, where the name resolved to a public address when you checked it and to an internal one a moment later when the browser connected
- Memory, because Chrome leaks and crashed contexts leave zombie processes, so the box that was fine for a week OOMs on a Sunday morning
- Cost, once someone finds an unmetered endpoint in your JS bundle: every request is a cold browser, and browsers are the most expensive thing per second you can run
- Fonts, which are absent from the container, so CJK and emoji render as boxes and nobody notices until a customer in Tokyo asks why their preview is empty
- Bot walls, where the honest result of your render is a Cloudflare challenge page, cheerfully stored and served as if it were the site
- The bucket, which fills with screenshots of pages strangers chose — a set of images you now host, cannot vouch for, and are answerable for
- Authenticated rendering, the feature everyone asks for next, which means callers hand you their session cookies and you write them into a request log
A support ticket comes in saying the screenshot came back as a wall of text instead of a website. The image is real: it is a JSON document, rendered in the browser's default monospace, and it contains an access key ID, a secret and a session token belonging to the instance role your renderer runs under. The URL in the request log is the link-local metadata address. It was not a customer who sent it — the key was found in a scraped bundle three weeks ago and the request came from a residential proxy in another country. Now the arithmetic: that role could read the screenshot bucket and, because it was easier at the time, the shared bucket next to it. You rotate the credentials in ten minutes, and then you spend the rest of the week reading CloudTrail, because the only question anyone will ask is what was read while the token was valid, and the honest answer is that you were not logging enough to know.
Is that you?
the verdict is a default, not a law
- The URLs come from you, not from callers — your own pages, your own OG images, on a fixed list
- It runs behind authentication, with a quota per key, and is not reachable from the open internet
- The renderer sits in a network that cannot reach anything private: no metadata endpoint, no RFC1918, egress through a proxy that enforces it
- You would be comfortable with the whole output bucket being public tomorrow
- Any caller can supply an arbitrary URL and you have not blocked private ranges at the network layer as well as in code
- The endpoint is unauthenticated or unmetered, because it will be found and used as a free proxy long before your users find it
- You are storing renders of arbitrary third-party pages under your own domain with no takedown path
- You are about to accept cookies or headers so it can render authenticated pages — you are asking strangers to send you their sessions
- It is on the same VPC as anything that trusts internal requests, including internal admin tools and unauthenticated metrics endpoints
If you build it anyway
the checklist, then the prompt that enforces it
- Enforce the network boundary in the network, not in your code. Run renderers in a subnet with no route to link-local or RFC1918 addresses and force egress through a proxy with an explicit policy — a URL check in application code is a suggestion, not a control.
- Disable the cloud metadata endpoint for the renderer, or require IMDSv2 with a hop limit of one so a browser inside the box cannot reach it.
- Re-validate on every redirect, not once at the start. Cap redirect depth, resolve hostnames yourself, pin the connection to the resolved public address, and re-check after each hop to close the DNS rebinding window.
- Restrict schemes to http and https. file, ftp, gopher, data and blob have no business in a URL a stranger supplies.
- Every request needs an authenticated key, a quota and a hard per-key rate limit, and every render needs a wall-clock timeout that kills the browser context rather than waiting politely.
- Run one browser context per request and recycle the process. Assume leaks; make the memory ceiling and the restart policy explicit rather than discovering them at 04:00.
- Cache aggressively by normalised URL plus options — most callers ask for the same twenty pages, and cache hits are the difference between a cheap service and an expensive one.
- Log the caller, the requested URL, the final URL after redirects and the response size, and keep an abuse contact and a takedown route. When your IP ends up on a blocklist, the log is the only thing that shortens the conversation.
- Strip or refuse caller-supplied cookies and auth headers. If you must support them, never write them to logs and never persist them beyond the request.
I am building a screenshot API: callers give it a URL, my server renders the
page in a headless browser and returns an image. Treat the URL as hostile input
that makes my infrastructure issue requests, and build the defences first.
1. Network first. Put the renderer in a subnet with no route to
169.254.169.254 or any RFC1918 range, force egress through a proxy with an
allow policy, and require IMDSv2 with a hop limit of one. Explain why an
application-level URL check is not enough on its own.
2. Then URL validation as defence in depth: http and https only, resolve the
hostname yourself, reject private, loopback, link-local and reserved
addresses, and connect to the address you resolved.
3. Follow redirects with a cap and re-run full validation at every hop. Show me
the test where an allowed host 302s to 127.0.0.1 and the request is refused.
4. Authentication and quota before the first public deploy: per-key rate limit,
monthly cap, hard concurrency ceiling. Assume the endpoint ends up in a
public JS bundle and is abused.
5. Per-request wall-clock timeout that terminates the browser context, plus a
process recycle policy and a memory ceiling. Chrome leaks; plan for it.
6. Cache by normalised URL plus render options, and tell me the expected hit
rate and what it saves.
7. Log caller key, requested URL, final URL after redirects, status and bytes.
Never log caller-supplied cookies or headers.
8. Store outputs in a private bucket under content-addressed names, served via
short-lived signed URLs, never from my app's own origin. Add a retention job
and a takedown path — I will be hosting images of pages I did not choose.
9. Ship a font package including CJK and emoji, with a test asserting a
non-Latin page does not render as boxes.
10. Refuse caller-supplied cookies or basic auth for rendering private pages.
Out of scope too: PDF export, video capture, proxy rotation, geolocation.
11. Finally, price it honestly for me: one always-warm browser instance per
month against $17 for 2,000 hosted screenshots.That one keeps you out of trouble. For the prompt that actually builds it, canivibecodeit.com has one.
their build prompt ↗Or don’t build it
the boring option, and the way back out
Almost immediately, and the maths is unusually clear. $17 a month covers 2,000 screenshots at under a cent each, billed only when a render succeeds. A machine with enough memory to keep one Chrome instance warm costs more than that before you have written a line of the SSRF filter, the redirect re-validation, the process recycler or the font packaging — and those are the parts that are dangerous rather than tedious. Self-host only when volume makes per-render pricing genuinely painful, and when you do, run Browserless or Gotenberg rather than your own harness.
$17/mo is cheaper than your weekend.
Keep the render request as a job record — normalised URL, options, output hash — and store images in object storage under content-addressed names, so the renderer itself is an interchangeable worker. Swapping to ScreenshotOne, Urlbox or a self-hosted Browserless is then a change of one adapter, and the cached outputs come with you. If you shut it down, do two things beyond deleting the code: revoke the instance role rather than just terminating the box, and empty the bucket — an abandoned public bucket of screenshots of other people's websites is somebody's takedown request eighteen months from now.
Dockerised headless-browser service with session management, timeouts and concurrency limits, dual-licensed SSPL and commercial.
Container API that converts URLs, HTML and documents to PDF and screenshots, with rendering timeouts built in.
Questions
Is SSRF really the first thing to worry about, ahead of cost?
Yes, because cost is recoverable and credentials are not. A runaway bill is an unpleasant email and a rate limit; a cloud role rendered into a PNG is an incident with a disclosure question attached. Do the network isolation first — no route to link-local or private ranges, IMDSv2 with a one-hop limit — and the worst remaining outcome is a large invoice.
I have an allowlist of domains. Am I fine?
Not on its own. An allowlisted host can redirect anywhere, and unless you re-validate after every hop the browser follows it for you. A hostname you resolved to a public address can resolve to a private one milliseconds later, which is DNS rebinding and it defeats check-then-connect entirely. Allowlists are useful as policy; the control that actually holds is a renderer that has no network path to anything private.
What is the safe version of this project?
One where you supply the URLs. OG image generation, thumbnails of your own marketing pages, PDF exports of documents you rendered yourself — all of that is a fixed list, an authenticated internal service, and genuinely a weekend. The verdict on this page is about the version that takes a URL from a caller, which is a different product with a different threat model wearing the same two lines of Playwright.
Why is buildEase higher than canivibecodeit's estimate?
Because the thing an agent hands you does work. `page.goto(url)` then `page.screenshot()` is twenty minutes and it renders real websites, which is exactly what makes this one of the more dangerous entries here — it is easy enough that you will actually deploy it, and the gap between that and a safe version is invisible until someone else finds it.
Every week, someone ships something they shouldn’t have.
New verdicts, the worst thing that landed in the trap, and the occasional incident report. No other email, ever.
An unattended screenshot timer is a small exfiltration device you built yourself. Aim it at a window, not the screen.
One scraper is a weekend. Forty scrapers is a job, and the site you are hammering never applied for it.
"Built-in bot evasion" is the product. Yours will be a CAPTCHA solver you told yourself was a cron job.
last reviewed 2026-08-04 · verdict is editorial and unsponsored · shared entry data from canivibecodeit under MIT · not legal advice