PST-092: The VAPID public key is derived from the secret key and handed to every signed-in member #821

Closed
opened 2026-10-10 10:56:59 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-092. Severity: Informational.

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Informational
  • Category: key-derivation / secret-exposure
  • Location: src/postulo/plugins/browser/webpush.py (91-105)
  • Found by: secrets and cryptography

Who can do it, and what they need. Any account holder on the instance (or anyone who obtains the HTML of the 'Browser' connection page). Precondition: the instance runs on a guessable key - POSTULO_ALLOW_WEAK_SECRET_KEY=true with a short/dictionary key, the dev settings, or a key that passes the length/variety check but is a …

How it works.

  1. Sign in as any member, open Settings -> Connections -> add 'Browser' notifier (GET /settings/connections/add/notifier/browser/).
  2. Read the data-web-push-key attribute on the card (verified in a scratch test: the exact base64url point from webpush.application_server_key() is in the page for a non-staff user).
  3. Offline, for each candidate key k: scalar = int(SHA-512('postulo-web-push-vapid:'+k)) mod (n-1) + 1; compare scalar*G with the published point. One hash and one scalar multiplication per guess, no salt, no stretching.
  4. …

Impact. On an instance whose operator used the documented weak-key override, a single member can escalate to every account and to every stored third-party credential. On a strong key there is no practical impact, which is why this is low.

Verification (verdict: confirmed; severity after verification: Informational; first-pass claim: Low). Keep the observation and drop the impact chain. The claim that SECRET_KEY lets an attacker 'forge Django session cookies / password-reset tokens for any account including administrators' does not hold here: sessions are database-backed with random ids, and reset tokens need the victim's password hash. What a recovered SECRET_KEY realistically gives is forged consent state values and forged flash-message cookies, plus decryption of stored secrets for anyone who also holds a database copy. The VAPID key is not an additional oracle for SECRET_KEY, since the signed messages cookie and the consent state are available to every member at the same per-guess cost. …

Suggested remediation. Generate the VAPID key pair once with ec.generate_private_key(), store the private half in an encrypted SiteSettings column (same Fernet store as the mail password) and publish only that public key; or derive it with HKDF from the key material plus a random per-instance salt kept in the database. Either way the published point stops being a pure function of SECRET_KEY. …

Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-092**. Severity: **Informational**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Informational - **Category:** key-derivation / secret-exposure - **Location:** `src/postulo/plugins/browser/webpush.py` (91-105) - **Found by:** secrets and cryptography **Who can do it, and what they need.** Any account holder on the instance (or anyone who obtains the HTML of the 'Browser' connection page). Precondition: the instance runs on a guessable key - POSTULO_ALLOW_WEAK_SECRET_KEY=true with a short/dictionary key, the dev settings, or a key that passes the length/variety check but is a … **How it works.** 1. Sign in as any member, open Settings -> Connections -> add 'Browser' notifier (GET /settings/connections/add/notifier/browser/). 2. Read the data-web-push-key attribute on the card (verified in a scratch test: the exact base64url point from webpush.application_server_key() is in the page for a non-staff user). 3. Offline, for each candidate key k: scalar = int(SHA-512('postulo-web-push-vapid:'+k)) mod (n-1) + 1; compare scalar*G with the published point. One hash and one scalar multiplication per guess, no salt, no stretching. 4. … **Impact.** On an instance whose operator used the documented weak-key override, a single member can escalate to every account and to every stored third-party credential. On a strong key there is no practical impact, which is why this is low. **Verification** (verdict: confirmed; severity after verification: Informational; first-pass claim: Low). Keep the observation and drop the impact chain. The claim that SECRET_KEY lets an attacker 'forge Django session cookies / password-reset tokens for any account including administrators' does not hold here: sessions are database-backed with random ids, and reset tokens need the victim's password hash. What a recovered SECRET_KEY realistically gives is forged consent `state` values and forged flash-message cookies, plus decryption of stored secrets for anyone who also holds a database copy. The VAPID key is not an additional oracle for SECRET_KEY, since the signed messages cookie and the consent state are available to every member at the same per-guess cost. … **Suggested remediation.** Generate the VAPID key pair once with ec.generate_private_key(), store the private half in an encrypted SiteSettings column (same Fernet store as the mail password) and publish only that public key; or derive it with HKDF from the key material plus a random per-instance salt kept in the database. Either way the published point stops being a pure function of SECRET_KEY. …
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:20 +00:00
Author
Owner

Fixed in 9d819450f on main.

Fixed in 9d819450f on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#821
No description provided.