Skip to content

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 HttpOnly and SameSite=Lax, and Secure in 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.

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.

security@dl2.app
Security · DL2App