PST-089: Board-recipe selection reads a different host from the one fetched when the URL carries userinfo #818

Closed
opened 2026-10-10 10:56:56 +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-089. 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: input-validation / spoofing
  • Location: src/postulo/plugins/builtin/boards/__init__.py (61-72)
  • Found by: completeness review: shipped plugins

Who can do it, and what they need. Someone who gets a member to capture a crafted link, or a holder of a captures token. No other precondition: page_address() accepts any http(s) URL including userinfo.

How it works.

  1. Attacker supplies https://www.linkedin.com:x@evil.example/jobs/view/1. page_address() accepts it (only the scheme and control characters are checked).
  2. recipe_for() takes urlparse(url).netloc.split(':')[0] and gets www.linkedin.com, so the LinkedIn recipe claims the page, while the fetcher and remembered places use hostname evil.example (verified in a scratch run).
  3. JobPostingData.source and the review screen show urlparse(url).netloc = www.linkedin.com:x@evil.example, and the stored URL is rendered as a link.

Impact. Cosmetic spoofing of which board a capture came from (a hostile page is parsed by LinkedIn's selectors and labelled as LinkedIn). No code execution or data access. core/link_services already refuses userinfo for exactly this reason; the capture path does not.

Verification (verdict: confirmed; severity after verification: Informational). Precondition: a member submits, or is talked into submitting, a URL with userinfo. Effect: the LinkedIn or Greenhouse recipe claims a page from another host, and the stored source and the capture URL link read as the board with @evil-host appended. There is no code execution, data access or SSRF. The suggested fix is to use urlsplit(url).hostname in recipe_for and source, or to refuse userinfo in page_address as link_services does. Confidence in the behaviour is high; its security significance is low.

Suggested remediation. Use urlsplit(url).hostname in recipe_for and for source, and reject or strip userinfo in page_address() for captured URLs.

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-089**. 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:** input-validation / spoofing - **Location:** `src/postulo/plugins/builtin/boards/__init__.py` (61-72) - **Found by:** completeness review: shipped plugins **Who can do it, and what they need.** Someone who gets a member to capture a crafted link, or a holder of a captures token. No other precondition: page_address() accepts any http(s) URL including userinfo. **How it works.** 1. Attacker supplies `https://www.linkedin.com:x@evil.example/jobs/view/1`. page_address() accepts it (only the scheme and control characters are checked). 2. recipe_for() takes `urlparse(url).netloc.split(':')[0]` and gets `www.linkedin.com`, so the LinkedIn recipe claims the page, while the fetcher and remembered places use hostname `evil.example` (verified in a scratch run). 3. JobPostingData.source and the review screen show `urlparse(url).netloc` = `www.linkedin.com:x@evil.example`, and the stored URL is rendered as a link. **Impact.** Cosmetic spoofing of which board a capture came from (a hostile page is parsed by LinkedIn's selectors and labelled as LinkedIn). No code execution or data access. core/link_services already refuses userinfo for exactly this reason; the capture path does not. **Verification** (verdict: confirmed; severity after verification: Informational). Precondition: a member submits, or is talked into submitting, a URL with userinfo. Effect: the LinkedIn or Greenhouse recipe claims a page from another host, and the stored `source` and the capture URL link read as the board with `@evil-host` appended. There is no code execution, data access or SSRF. The suggested fix is to use `urlsplit(url).hostname` in `recipe_for` and `source`, or to refuse userinfo in `page_address` as link_services does. Confidence in the behaviour is high; its security significance is low. **Suggested remediation.** Use urlsplit(url).hostname in recipe_for and for `source`, and reject or strip userinfo in page_address() for captured URLs.
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:18 +00:00
Author
Owner

Fixed in ddafcbe0e on main.

Fixed in ddafcbe0e 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#818
No description provided.