Webhooks can't reach internal addresses; sign-up doesn't reveal which emails have accounts; staff-only pages are staff-only on the server; bad API input is a 400, not a 500.
**Webhooks can't reach internal addresses; sign-up doesn't reveal which
emails have accounts; staff-only pages are staff-only on the server; bad
API input is a 400, not a 500.**
Security
- Webhook targets must be public addresses (SSRF). A webhook URL that resolves to loopback, private (10/8, 172.16/12, 192.168/16, fc00::/7), link-local (incl. the cloud metadata service at 169.254.169.254), CGNAT, reserved or multicast addresses — directly, through DNS, or via IPv4-mapped / 6to4 / NAT64 IPv6 or decimal/hex IP spellings — is refused when the webhook is saved (Settings → Webhooks and
POST /api/v1/webhooks/→ 400) and again on every delivery. Each request connects to the exact IP that passed the check (DNS rebinding can't redirect it), keeps the host name for theHostheader and TLS verification, and redirects are never followed (a 3xx is logged as a failed delivery). Blocked deliveries are logged as failed with the reason, shown in the delivery log; no connection is made. New check:apps/api/url_safety.py.WEBHOOK_ALLOW_PRIVATE_TARGETS=truelets a development machine target a local receiver (metadata addresses stay blocked). **Existing webhooks that point at private or local hosts stop delivering** — each attempt is logged as "Blocked". - Registration no longer reveals whether an email has an account. A taken address gets the same "Check your email" page as a new one, and its owner is emailed "someone tried to sign up with your address — sign in or reset your password". New accounts are no longer signed in straight after the form: the verification link signs them in (or sign in with your password and use “Resend”). Taken usernames are still reported. Password reset was already neutral.
- Staff-only pages are enforced on the server, not just hidden. The CRM, eCommerce and SaaS dashboards, the charts showcase, the onboarding wizard and the forms, widgets, datatable, API-docs and maps showcase pages were hidden from non-staff in the sidebar and command palette but opened for any signed-in account by URL. They now answer like the other staff-only pages: anonymous → login, non-staff → 403. The views use the sidebar's own
requires_staffflag throughNavItemAccessMixin/@nav_access_required(apps/core/navigation.py), so the menu and the server can't disagree. The marketing pages and the coming-soon, maintenance and 503 pages stay public by design; Overview and Analytics stay open to every account.
Fixed
- API writes validate before saving.
POST /api/v1/products/withoutcategory_id(or with an unknown one) returned 500; so did a negative stock, a second product with the same name (empty slug; slugs are now generated from the name),{"name": null}on `PATCH /api/v1/customers/<id>/`, and over-long values on PostgreSQL. These are now400 {"detail": "field: message"}; invalidstatusvalues and malformed customer emails, which were stored silently, are refused the same way. Anything the database still refuses maps to a 400.
Docs
- Webhook deliveries are one attempt per event — a 5-second synchronous POST, no automatic retries, every attempt in the delivery log. The API docs used to promise "retries with exponential backoff", which never existed. To add retries, run
dispatch_webhook()from a task queue. Stated on the API docs page, Settings → Webhooks, the OpenAPI description and the README. - Unit-test counts refreshed (1401).
If you've built on these surfaces: point any local/private webhook at a
public URL (or set WEBHOOK_ALLOW_PRIVATE_TARGETS=true in development
only), note that sign-up now lands on /accounts/register/done/
without signing the user in, and give is_staff to any account that
should keep the staff-only dashboards and showcase pages. No migrations.