SEC2-028: The webhook notifier accepts a plain http:// receiver, so notification text leaves in clear #970

Closed
opened 2026-10-17 10:38:44 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-10-16 in a read-only security review of postulo at 56fc9d531, checked by a second, independent pass.

Severity: Low · Category: Data Exposure · Reference: CWE-319 (Cleartext Transmission of Sensitive Information); OWASP A02:2021 (Cryptographic Failures)

Location:

  • src/postulo/plugins/webhook/__init__.py:185-192 (validate: url.startswith(("https://", "http://")), and the message "An address starting with https:// or http://.")
  • src/postulo/plugins/webhook/__init__.py:164-167 (the help text says "HTTPS")
  • src/postulo/plugins/webhook/__init__.py:101-121 (payload_for: title, body, URL and data of every event)

Issue

The field's help promises "HTTPS, and a public address unless the operator has allowed private ones", and the module docstring describes the signature as what lets a receiver tell Postulo from "anyone who found the address". validate nevertheless accepts http://, and its error message contradicts the help outright by asking for "https:// or http://". The HMAC authenticates the body to the receiver; it does not hide it, and the body is the notification: the role, the company, an interview's time and place, an offer's terms, a reminder's words. The reference plugins and the wiki teach the HTTPS form, and _links() in plugins/views.py already refuses a non-https address for a connection card, so the rule exists elsewhere.

Impact

A person who types an http:// receiver (a home automation box, a webhook relay) sends their job-search events over the network in clear to anyone on the path between the instance and the receiver. Their own choice, against the field's stated rule.

Fix

Refuse http:// unless the operator allowed private destinations and the address is in fact private (a LAN receiver is the one honest case; check_destination already resolves the address, so a plain http:// to a public host is refused regardless):

if not url.startswith("https://") and not (
    url.startswith("http://") and destinations.private_allowed() and _is_private(url)
):
    problems["url"] = [str(_("An address starting with https://."))]

and make the help text and the error message say the same thing. Done when: tests/test_webhooks.py asserts validate({"url": "http://example.org/hook", ...}) returns a problem for url on a default instance, and still does with private destinations allowed, since the host is public.

#365 (open) - the delivery's deadline and size cap, a different weakness of the same plugin. #779 (closed) - the private-address switch the exception rests on. Nothing on the scheme.

Found on 2026-10-16 in a read-only security review of `postulo` at `56fc9d531`, checked by a second, independent pass. **Severity:** Low · **Category:** Data Exposure · **Reference:** CWE-319 (Cleartext Transmission of Sensitive Information); OWASP A02:2021 (Cryptographic Failures) **Location:** - `src/postulo/plugins/webhook/__init__.py:185-192` (`validate`: `url.startswith(("https://", "http://"))`, and the message "An address starting with https:// or http://.") - `src/postulo/plugins/webhook/__init__.py:164-167` (the help text says "HTTPS") - `src/postulo/plugins/webhook/__init__.py:101-121` (`payload_for`: title, body, URL and `data` of every event) ## Issue The field's help promises "HTTPS, and a public address unless the operator has allowed private ones", and the module docstring describes the signature as what lets a receiver tell Postulo from "anyone who found the address". `validate` nevertheless accepts `http://`, and its error message contradicts the help outright by asking for "https:// or http://". The HMAC authenticates the body to the receiver; it does not hide it, and the body is the notification: the role, the company, an interview's time and place, an offer's terms, a reminder's words. The reference plugins and the wiki teach the HTTPS form, and `_links()` in `plugins/views.py` already refuses a non-`https` address for a connection card, so the rule exists elsewhere. ## Impact A person who types an `http://` receiver (a home automation box, a webhook relay) sends their job-search events over the network in clear to anyone on the path between the instance and the receiver. Their own choice, against the field's stated rule. ## Fix Refuse `http://` unless the operator allowed private destinations *and* the address is in fact private (a LAN receiver is the one honest case; `check_destination` already resolves the address, so a plain `http://` to a public host is refused regardless): ```python if not url.startswith("https://") and not ( url.startswith("http://") and destinations.private_allowed() and _is_private(url) ): problems["url"] = [str(_("An address starting with https://."))] ``` and make the help text and the error message say the same thing. Done when: `tests/test_webhooks.py` asserts `validate({"url": "http://example.org/hook", ...})` returns a problem for `url` on a default instance, and still does with private destinations allowed, since the host is public. ## Related #365 (open) - the delivery's deadline and size cap, a different weakness of the same plugin. #779 (closed) - the private-address switch the exception rests on. Nothing on the scheme.
tiagoagueda added this to the 0.6.0 milestone 2026-10-17 10:38:45 +00:00
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#970
No description provided.