# Screenshot endpoint safety review — public report specimen

Status: Latchshot-authored worked specimen for an intentionally vulnerable route. It is not customer work, a live vulnerability disclosure, a penetration-test result, a certification, or evidence of a paid engagement.

This specimen shows how the fixed-scope $149 review ranks evidence and records the patch boundary before a buyer shares a public repository. A real review cites the buyer's exact public source, follows its existing tests and conventions, and includes the agreed focused patch.

## Scope record

- Repository: illustrative only; no customer repository was reviewed.
- Route: `src/routes/screenshot.ts`.
- Runtime: local Playwright.
- Input: caller-controlled `url` query parameter.
- Output: synchronous PNG bytes.
- Review surface: the route, its browser call, direct request graph, caller authorization, and resource bounds.
- Excluded: live exploitation, private infrastructure, cloud accounts, customer data, compliance certification, and unrelated application routes.

## Intentionally vulnerable fixture

```ts
app.get('/api/screenshot', async (request, response) => {
  const page = await browser.newPage();
  await page.goto(String(request.query.url));
  const png = await page.screenshot({ type: 'png' });
  await page.close();
  response.type('png').send(png);
});
```

The snippet is evidence for this specimen, not a safe implementation or copy-paste starting point.

## Executive summary

| Rank | Finding | Why it matters | Patch gate |
| --- | --- | --- | --- |
| Critical | Arbitrary browser egress | The main document, redirects, frames, and subrequests can reach internal or metadata services and return their rendered contents | Enforce destination policy at connection time for the whole request graph |
| High | No caller authorization | Any network caller can spend browser capacity and request captures | Authenticate the caller and authorize the capture action before opening a page |
| High | Unbounded browser work | No queue, concurrency, timeout, dimension, or response-size boundary limits resource amplification | Add explicit admission and job bounds with deterministic failure |
| Medium | Undefined error and retention policy | Raw browser failures can disclose target detail; output lifetime and cleanup are unspecified | Sanitize operational errors and document artifact/log retention |

The critical finding blocks release. The two high findings block an internet-facing deployment. The medium finding should be resolved in the same route handoff because it affects incident evidence and data handling.

## Finding 1 — arbitrary browser egress

### Evidence

`request.query.url` reaches `page.goto()` without a scheme, credential, hostname, port, address, DNS, redirect, frame, or subrequest policy. Chromium performs more outbound connections than the first URL alone.

### Impact

A caller can attempt loopback, RFC1918, link-local, cloud-metadata, IPv6 local, IPv4-mapped IPv6, special-use, and internal DNS destinations. A public-looking hostname can resolve to mixed public/private answers, change between validation and connection, redirect, or load a private subresource.

### Required control

Do not treat a hostname regex, one preflight DNS lookup, a final-URL check, or response filtering as sufficient. The agreed patch must either:

1. narrow the product contract to a fixed trusted origin and block every cross-origin navigation/subrequest; or
2. validate every DNS answer and enforce the approved destination at connection time through a pinned connection or egress proxy across initial navigation, redirects, frames, and subrequests.

The exact choice depends on the repository's real contract and deployment network. A review should not invent general-purpose proxy infrastructure inside a fixed-price route patch.

## Finding 2 — missing authorization

### Evidence

The fixture opens a browser before authenticating a caller or authorizing a screenshot action.

### Impact

An unauthenticated caller can consume CPU, memory, network, and output bandwidth. Network filtering alone would not decide whether an otherwise public capture is allowed for that user or account.

### Required control

Verify the application credential, product action, and per-caller quota before reserving browser work. Return the application's ordinary `401` or `403` shape without echoing the submitted target.

## Finding 3 — unbounded browser work

### Evidence

The fixture has no admission limit, queue cap, total job deadline, navigation deadline, fixed dimensions, output byte cap, or guaranteed cleanup path.

### Impact

Slow pages, large canvases, repeated requests, downloads, popups, or browser failures can hold resources and degrade unrelated requests.

### Required control

- reserve from a bounded queue before creating the browser context;
- use one total deadline that also covers navigation and capture;
- keep viewport, full-page growth, and output size within published limits;
- block service workers, downloads, popups, and unneeded request types where the contract permits;
- close page/context resources in `finally`; and
- return explicit overload and timeout errors without an unbounded retry loop.

## Finding 4 — error and retention boundary

### Evidence

The fixture has no error mapping, logging rule, artifact-retention rule, or cleanup evidence.

### Impact

Raw errors can expose a target URL, signed query, internal hostname, or browser detail. Unspecified artifact retention makes incident response and deletion behavior ambiguous.

### Required control

Record only the operational fields needed by the application, strip URLs and credential-shaped values from error text, and state whether the PNG is streamed, stored, or deleted. Never log authorization headers or persist a private/signed target in a public issue or specimen.

## Focused patch contract

The review's patch would remain inside the route and its direct target-policy or egress helper. The following is an interface sketch, not a complete security implementation:

```ts
const caller = await requireCapturePermission(request);
const permit = await captureQueue.reserve(caller.id);

try {
  const target = await targetPolicy.authorize(String(request.query.url));
  const result = await guardedBrowser.capture(target, {
    width: 1200,
    height: 630,
    timeoutMs: 10_000,
    maxOutputBytes: 8_000_000,
  });
  response.type('png').send(result.bytes);
} finally {
  permit.release();
}
```

`targetPolicy.authorize()` and `guardedBrowser.capture()` are deliberately unspecified here. Claiming safety requires inspecting how the real repository resolves and connects—not naming a helper and assuming the boundary exists.

## Regression matrix

| Case | Expected evidence |
| --- | --- |
| Approved public control | `200 image/png`, fixed dimensions, non-zero bounded bytes |
| Missing caller credential | `401`, no browser reservation |
| Caller lacks product action | `403`, no browser reservation |
| Non-HTTP scheme or URL credentials | rejected before DNS or browser work |
| Loopback, RFC1918, link-local, and metadata addresses | rejected across IPv4 and IPv6 forms |
| IPv4-mapped IPv6 local address | rejected |
| Mixed public/private DNS answers | whole target rejected |
| Public hostname that re-resolves privately | connection blocked rather than using the new answer |
| Public redirect to private destination | redirect blocked |
| Public page with private frame/image/script | subrequest blocked |
| Queue full or job deadline reached | explicit overload/timeout response; resources released |
| Browser error contains a URL or key-shaped token | sanitized response and log detail |

Focused tests run in the same network boundary as the browser or egress proxy. A mocked URL parser alone cannot prove connection-time enforcement.

## Verification and rollback record

The completed customer handoff records:

1. exact repository checks and focused test results;
2. the accepted public control artifact type, dimensions, byte size, and timing;
3. negative-path results for the agreed address, DNS, redirect, subrequest, authorization, and bound cases;
4. the changed files and configuration;
5. residual risks and any infrastructure control the source patch cannot prove; and
6. the shortest route-level rollback with the former configuration retained until acceptance.

The buyer reviews, merges, deploys, verifies, and rolls back. The review never requests repository credentials or production access.

## Residual risk

Passing the focused matrix does not prove the endpoint or application is vulnerability-free. Browser, DNS, proxy, container, cloud-egress, authentication, storage, logging, and dependency behavior can change outside the reviewed route. The handoff identifies those dependencies without presenting the review as a pentest, certification, guarantee, or continuing monitoring service.

## Commercial boundary

This specimen makes the report format inspectable. It does not claim a customer, payment, completed paid review, production patch, vulnerability discovery in a real project, savings result, or endorsement. Fit, exact scope, payment, and start are confirmed by the owner before any repository work begins.
