Security
What is in place today
A list of controls that exist in the product now, written so each one can be checked. What we have not done yet is at the bottom.
No full IP address is stored for a tap.
Last changed 10 October 2026.
Accounts and sign-in
5 controls
- Passwords are stored only as bcrypt hashes (cost 12) and must be at least 10 characters.
- Two-factor sign-in with an authenticator app (TOTP), with single-use recovery codes. The secret is encrypted with AES-256-GCM before it is stored, and a code cannot be used twice.
- Sign-in with Google or GitHub uses the authorization-code flow with PKCE and a state check.
- Session cookies are
HttpOnlyandSameSite=Lax, andSecurein production. Only a SHA-256 hash of the session token is stored, sessions end after 30 days, and signing out deletes the session on the server. - Emailed links (password reset, sign-in links, email confirmation) are single-use, expire, and are stored only as hashes.
Teams and access
3 controls
- Three roles (owner, admin, member). Every change checks the signed-in person’s role on the server, not just in the page.
- An activity log records who changed what, on every plan.
- Staff tools require a staff flag that is re-read from the database on every request, and every staff action is written to the same log.
API keys and webhooks
4 controls
- API keys are shown once and stored as SHA-256 hashes. Secret keys carry scopes, so a key can be limited to reading.
- Publishable keys, the kind that ship inside an app, can only call the SDK endpoints.
- The API is rate limited per key, and accepts idempotency keys so a retried request is not applied twice.
- Webhook deliveries are signed (HMAC-SHA256 with a timestamp), secrets can be rotated with an overlap period, and a webhook URL that resolves to a private network address is refused.
Links and the people who tap them
7 controls
- Destinations must be
httpsand cannot point at private or local addresses. - Every destination is scanned when a link is created or changed, and active links are scanned again every 24 hours: lookalike and punycode domains, bare IP addresses, shortener chains, sign-in wording next to brand names, a staff deny list, and Google Web Risk when it is configured. Malicious destinations are refused or flagged; suspicious ones go to a person.
- New accounts are limited in how many links they can create per hour and per day, and a team is put on hold automatically when several of its links are flagged within a day.
- Anyone can report a link without an account. We aim to review reports within 24 hours; the form accepts 5 reports an hour from one address.
- Pages served on link addresses send a strict Content-Security-Policy with per-response nonces, refuse framing, and send
X-Content-Type-Options: nosniffand, in production, HSTS. - Password-protected links store a bcrypt hash of the password, and guesses are rate limited per link and address.
- Subdomains that imitate well-known brands or sign-in pages are reserved and cannot be registered.
Visitor data
3 controls
- No full IP address is stored for a tap: only a truncated network and a hash keyed with a secret that changes daily. Details are on the privacy page.
- The SDKs read no advertising identifier and request no tracking permission.
- Raw user-agent strings are not stored with clicks.
Server-side requests
4 controls
- When we fetch something from a customer’s domain (to verify a website, run the public link checker, or deliver a webhook), the hostname is resolved first and the request is refused if it points at a private, loopback or link-local address.
- Stripe webhooks are accepted only with a valid signature and a recent timestamp, and each event is processed once.
- CSV exports neutralise cells that a spreadsheet would run as formulas.
- Values from requests reach the database only as query parameters, and form and API input is validated against a schema before it is used.
Not yet
5 gaps
We would rather you know than assume:
- No independent audit or certification (such as SOC 2) and no third-party penetration test.
- No single sign-on (SAML) and no SCIM.
- No published uptime commitment or status history.
- No choice of hosting region.
- No bug bounty. We do read and answer reports.
Reporting a vulnerability
Write to security@dl2.app with what you found and how to reproduce it. Please give us a reasonable time to fix it before publishing, and do not access other people’s data or disrupt the service while testing. We will not take legal action against good-faith research that follows those two rules. To report a link that is being abused rather than a flaw in DL2App, use the report form.