Skiplink

Security

Skiplink loads pages that strangers control, in a browser, on our infrastructure. The design starts from the assumption that one of those pages will eventually win.

Reporting a vulnerability

Email security@skiplink.dev. Include enough for us to reproduce it. You will get a human acknowledgement within 2 working days and an assessment within 5.

Safe harbour

If you research in good faith, stay within your own accounts and test data, avoid degrading the service for anyone else, do not access or exfiltrate other people's data, and give us reasonable time before publishing, then: we will not pursue legal action, we will not report you, and we will credit you if you would like us to.

If you accidentally reach data that is not yours, stop, tell us, and delete it. That is a finding, not an offence, and we will treat it as one.

We have no bug bounty budget yet. We will say so honestly rather than imply a reward that does not exist.

Out of scope: reports generated only by a scanner with no demonstrated impact, missing headers with no exploit path, social engineering of our staff, denial of service, and anything requiring a compromised device belonging to the victim.

The parts of the design that are security decisions

Two machines, and the split is the point

The API box holds the database, the queue and the payment keys, and never fetches a customer-supplied URL. A separate box runs the browsers, and holds no credentials at all — no database access, no API keys, nothing but the job it was handed. It cannot reach private address space, by firewall rather than by policy.

So the machine most likely to be attacked is the one with nothing on it, and everything it knows arrives in the job and leaves in the result.

Two independent layers against server-side request forgery

A scanning API is an arbitrary server-side fetch, which is the most abusable primitive a service can offer. Every URL is resolved before it is fetched and refused if any answer points at loopback, RFC1918, link-local, CGNAT, unique-local, multicast or a cloud metadata address — including via IPv6 transition addresses, and re-checked at every hop of a redirect chain rather than only on the first request.

That layer cannot be complete: DNS rebinding beats any application-level check by construction, because the name resolves differently between the check and the connection. So the second layer is the firewall on the browser box, which does not consult DNS and does not care what any hostname claims.

Secrets are stored as hashes

Passwords are argon2id. API keys, session tokens, email verification tokens and password reset tokens are stored as hashes with an indexed prefix for lookup. A dump of our database does not yield a working credential.

Backups that have been restored

Nightly, and each one is restored into a scratch database with its rows counted before it counts as a backup. This caught a real problem the first time it ran, which is the argument for doing it.

Content Security Policy on the website

default-src 'none', then only what the site uses. No CDN, no external fonts, no analytics — a tracker added by accident would fail to load. Inline scripts are allowed by SHA-256 hash rather than by 'unsafe-inline', and a script that checks every page under the real policy in a real browser runs before release, because a wrong hash silently breaks the page rather than warning.

What we do not have

No ISO 27001, no SOC 2, no penetration test report. We are small, and those cost money we have not yet made. If your procurement requires one, we will tell you we do not have it rather than gesture at a roadmap.

There is no bug bounty. There is no 24/7 on-call rota. There is one person, who reads security@skiplink.dev.

If something happens

We will tell affected customers within 48 hours of becoming aware of a breach involving their data, with what we know at the time, including the parts we do not yet understand. Regulator notification follows the statutory 72 hours where it applies. The commitment is in the data processing addendum, which makes it contractual rather than a good intention.