Skip to content
DryDock Kit
  • Proof
  • Walkthrough
  • Features
  • Why DryDock
  • Pricing
  • FAQ
  • Docs
Pricing →
  • Proof
  • Walkthrough
  • Features
  • Why DryDock
  • Pricing
  • FAQ
  • Docs
Pricing →

DryDock Kit

Security write-up

DryDock Kit has had two security reviews, both in-house: the 1.0 audit (20 findings) and a post-1.0 adversarial review (15 findings). Every finding is listed here with its fix and its regression test. Neither review was an independent penetration test, and nothing here claims to make your application secure.

How the reviews worked

The 1.0 audit read every server entry point (Server Actions, API routes, services and the repositories they call), then tested each boundary with the attacker's question: can the wrong user do this? Each fix has a regression test that was run with the fix removed and seen to fail before the fix went back in. Nothing was rated critical: no finding let an outside attacker read or change another tenant's data, bypass sign-in, or forge a payment.

The post-1.0 review was done by someone on the project deliberately trying to break it from the source. Every high and medium fix, and the low ones marked so in SECURITY.md, was tested the same way: fix removed, test fails; fix back, test passes.

The security suite runs in CI on every push. On 2 October 2026 it was 244 tests in 24 files, all passing, against PostgreSQL 16.

The 1.0 audit: 20 findings

2 high, 8 medium, 8 low, 2 informational. One low finding is accepted and documented (F-08); one is guarded by a test (F-20); the rest are fixed.

Findings from the 1.0 security audit
IdFindingFixTest
F-01HighAn admin could invite an address they control as owner, accept it, and gain owner rights such as deleting the organization and managing billing.Only an owner can invite someone as owner, enforced in the invitation service.authorization.security.test.ts
F-02MediumWith two or more owners, an admin could remove or demote one of them.Nobody can act on a member with a higher role.authorization.security.test.ts
F-03MediumThe seat limit was checked when an invitation was sent and ignored pending invitations, so a 10-seat plan could reach 29 members.Pending invitations reserve seats, and acceptance re-checks under a per-organization lock.billing.security.test.ts, including a concurrent race for the last seat
F-04MediumEmails inserted user-controlled names without escaping, allowing HTML injection sent from the product's real domain.Every value in an email template is HTML-escaped.input-output.security.test.ts
F-05MediumAPI-key rate limiting counted every attempt by key id, so 20 bad requests a minute locked out the real key.Only failed attempts count, per key and per IP; a valid key always authenticates.api-keys.security.test.ts
F-06MediumWebhook routes read request bodies of any size before checking the signature.Bodies over 1 MiB are refused with 413.webhooks.security.test.ts
F-07MediumA suspended organization's API keys still authenticated.Key authentication checks the organization's suspension in the same query.api-keys.security.test.ts
F-08LowAcceptedSession tokens are stored unhashed, as Better Auth stores them.Not fixable without replacing Better Auth's sessions. A token from the database alone doesn't sign anyone in without AUTH_SECRET; keep backups and .env apart.authentication.security.test.ts pins both facts
F-09LowNotification actions trusted a page size and type sent by the browser.Page size is clamped to 1–100 and the type is checked against the catalogue.input-output.security.test.ts
F-10LowBetter Auth switches its CSRF origin check off in tests, so no test could catch a regression.The origin check is pinned on in every environment.authentication.security.test.ts
F-11LowThe production server started with an invalid environment and served errors instead of failing fast.Startup validates the environment and exits on a problem.Verified against the production image
F-12LowA late webhook could move an invoice or payment backwards, from paid to open.Impossible status changes are refused; all 50 transitions are tested.status-transitions.test.ts, billing.security.test.ts
F-13LowAPI errors could include internal details for unexpected failures.Internal errors never include details.input-output.security.test.ts
F-14LowA revoked API key could be rotated into a working one.Revoked keys can't be rotated, and revoking twice changes nothing.api-keys.security.test.ts
F-15InfoSome controls the docs described didn't exist: secret scanning in CI, a lint rule for raw SQL, body-size limits.Each one was built rather than deleted from the docs.secret-scan.test.ts, architecture.test.ts
F-16MediumBehind a proxy that appends X-Forwarded-For, every sign-in shared one rate-limit bucket, so one client could lock everyone out.The client IP is read from X-Real-IP first.authentication.security.test.ts
F-17HighThe public example AUTH_SECRET passed validation, so a misconfigured server could run with it and anyone with the download could mint verification links.Production refuses to start with it, and saas:check fails on it, which stops deploy.sh.env.test.ts, saas-check/security.test.ts
F-18MediumRazorpay plan changes were ignored until the next renewal, so a paid upgrade wasn't delivered.Razorpay's subscription.updated now moves the subscription to the new plan.razorpay-webhooks.test.ts
F-19LowThe test for switching organizations passed without exercising the membership check.Replaced by a test that runs the real check and fails without it.org-switching.security.test.ts
F-20InfoGuardedA seed and test helper grants a paid plan without payment; if any page called it, any owner could upgrade for free.A test fails if any page, action or route imports it.billing-exposure.security.test.ts

The post-1.0 review: 15 findings

4 high, 3 medium, 8 low, all fixed. Seven informational items were also resolved: a nonce-based script policy, “trust this device” ignored for admins, session revocation without sending tokens to the browser, dependency clean-up, symlinks refused by the MCP server, recipient addresses kept out of email error logs, and sign-up answering the same way for existing addresses.

Findings from the post-1.0 adversarial review
IdFindingFixTest
H1HighThe admin second-factor gate checked the account, not the session, so a magic link, social sign-in or trusted device reached /admin without a code./admin requires a second factor verified on the session itself.second-factor.security.test.ts
H2HighImpersonation could become a lasting takeover: the admin could add a passkey or change the password, and dropping a cookie stretched 30 minutes to 7 days.Sensitive actions are refused while impersonating, the session ends at 30 minutes, and audit rows name the admin.impersonation.security.test.ts
H3HighPaddle one-time purchases were resolved from data the buyer controls, so a €9 transaction could grant a lifetime plan.Every provider's purchases resolve only from the checkout the app recorded, with product and amount checked.purchase-resolution.security.test.ts
H4HighA Paddle webhook hint could rewrite another organization's billing customer.Customer mappings are create-only and never overwritten by a webhook.purchase-resolution.security.test.ts
M1MediumAn open redirect after sign-in: a tab character in the path became //evil.Control characters and backslashes are refused, and the target must be same-origin.open-redirect.security.test.ts
M2MediumTokens in page URLs reached PostHog and Sentry, and a form submitted before the page loaded could put a password in the URL.URLs are scrubbed before analytics and error reports, and every credential form posts.url-sanitizer.test.ts, form-method.test.ts
M3MediumTwo billing jobs running at once could bill one usage report twice.One run at a time, and each report is sent only by the run that leases it.billing-cron.security.test.ts
L1LowRefunds and chargebacks didn't take back what a purchase granted.A reversal revokes the grant, even if it arrives before the purchase is recorded.billing-reversals.test.ts
L2LowMetered usage since the last billing run went unbilled after a plan change or cancellation.Outstanding usage is reported first.billing-cron.security.test.ts
L3LowThe live-notification stream had no per-user connection cap.5 streams per user (per instance), and a 1-second minimum poll.notification-stream.test.ts
L4LowThe waitlist revealed whether an address was already on it.The same answer either way.growth.security.test.ts
L5LowRoadmap votes could race, bad ids caused server errors, and voting was unlimited.Idempotent votes, not-found for bad ids, and 30 votes a minute per user.growth.security.test.ts
L6LowUnknown blog and help pages returned server errors on a production build.They return not-found, and pages that need a user redirect to sign-in.blog-content.test.ts, headers.security.test.ts
L7LowMagic-link and password-reset emails had no per-address limit.3 of each per address every 15 minutes; magic-link sign-ups count against the sign-up limit.auth-email-limits.security.test.ts
L8LowPaddle's own signature check wasn't constant-time and failed during secret rotation.The kit's own constant-time check, accepting any matching signature within 5 minutes.paddle-signature.security.test.ts

Known limitations

These are open, and documented in the download so you can decide what matters for your product:

  • Two copies of the same webhook arriving at the same moment can both be processed. Unique keys keep the effect single (tested), but the late copy can fail with a 500 and stay marked failed until the provider retries it.
  • Session tokens are stored unhashed (F-08). Using one needs both the database and AUTH_SECRET.
  • The Content Security Policy restricts scripts with nonces but still allows inline styles.
  • Sign-in rate limits are per IP and path, so credential stuffing spread across many IPs isn't caught per account.
  • Client IP trust depends on your reverse proxy setting X-Real-IP, and on the app being reachable only through it.
  • Nothing asks for the password again before sensitive actions such as creating an API key; account deletion does.
  • A burst of simultaneous invitation acceptances for one organization can time out. It fails closed: the seat limit is never exceeded.
  • No test runs a real OAuth round trip with a live provider.
  • The cap of 5 notification streams per user is per instance.
  • A refund or chargeback revokes unused credits but doesn't claw back credits already used.
  • Paddle's localized currencies aren't supported for one-time products; such a purchase fails closed and needs a human.

There are no compliance certifications (SOC 2, PCI DSS and so on). Card details are entered in the payment provider's own checkout, never in the app.

Reporting a vulnerability

Email support@drydockkit.com with “SECURITY” in the subject, never in public. Include the version, the affected part and the steps to reproduce. Fixes ship as patch releases whose changelog entry says what to do, and every buyer is emailed when one ships.

The full SECURITY.md, with the threat model, every control and the review checklist, ships in the download.

DryDock Kit

The Next.js starter kit for multi-tenant B2B SaaS. Organizations, roles and billing, with the tests that prove them.

Support: support@drydockkit.com

Release notes by email

New versions and editions only. Unsubscribe any time. Privacy policy

Product

  • Proof
  • Walkthrough
  • What's included
  • Why DryDock Kit
  • Pricing

Resources

  • Documentation
  • Changelog
  • Security write-up
  • Support
  • FAQ

Legal

  • License
  • Terms of sale
  • Refund policy
  • Privacy
© 2026 DryDock Kit · Made by Abinithish Kumardrydockkit.com