Rate limits: link checks, logo addresses and Find logo make the server fetch person-chosen URLs with no limit #407

Closed
opened 2026-10-01 10:46:28 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, listing every throttle.consume and throttle.capture call and every caller of public_only_client and fetching.fetch_page.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at jobs/forms.py:308-312 (apply_logo); the other places are where they were.

#112 and #194 bounded how often an account can make the server fetch an address it chose, and POSTULO_CAPTURE_RATE is spent by the capture form and the capture API. Three other paths make the server fetch person-chosen public URLs and spend nothing: Check and Check all on career links, the logo address on the company form, and Find logo / Refresh. Related to #112 and #194 (closed), which covered capture and the API only.

  • Where: resume/views.py:363-391 (LinkCheckView.post) and resume/links.py:47-56 (check_all: one HEAD, and a GET after 401, 403, 405, 501 or 5xx, for every link the account has, each following up to three redirects, inside the request); jobs/forms.py:307-311 (apply_logo calls logos.from_url whenever the logo address changes); jobs/views.py:450-467 (CompanyLogoActionView.post sends a logo errand) and jobs/logos.py:297 (find_on_website: the company page through fetching.fetch_page, then up to six images). None of these calls throttle.
  • What happens: an account holder with a few hundred links pressing Check all repeatedly, or re-saving companies with changing logo addresses, makes the instance send hundreds of requests per click to hosts of their choosing, from the operator's address, with no ceiling.
  • Who can trigger it: any signed-in account; nothing here is reachable without one. It matters on an instance with more than one person, where one account can spend the operator's reputation and bandwidth on a third party.
  • Why it matters: the capture limit exists to stop exactly this, and the wiki (Hardening, Configuration) tells operators capture is the only such path, so the bound they rely on is incomplete.
  • Fix: spend a fetch allowance once per outbound URL in links.check, in logos.from_url and in CompanyLogoActionView.post before the errand is sent, either against POSTULO_CAPTURE_RATE or against a new POSTULO_FETCH_RATE of its own. Check all stops when the allowance runs out and says how many links it did not check. Name the new consumers in Configuration → Rate limits and Hardening.

Done when

  • A test in tests/test_links_and_letters.py sets the rate to 2/h, stores three links, posts Check all, and asserts only two requests were made and the message says one was not checked.
  • A test in tests/test_logos.py asserts that saving a company with a new logo address and pressing Find logo each spend the allowance and are refused with a sentence once it is spent.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, listing every `throttle.consume` and `throttle.capture` call and every caller of `public_only_client` and `fetching.fetch_page`. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `jobs/forms.py:308-312` (`apply_logo`); the other places are where they were. #112 and #194 bounded how often an account can make the server fetch an address it chose, and `POSTULO_CAPTURE_RATE` is spent by the capture form and the capture API. Three other paths make the server fetch person-chosen public URLs and spend nothing: *Check* and *Check all* on career links, the logo address on the company form, and *Find logo* / *Refresh*. Related to #112 and #194 (closed), which covered capture and the API only. - **Where:** `resume/views.py:363-391` (`LinkCheckView.post`) and `resume/links.py:47-56` (`check_all`: one `HEAD`, and a `GET` after 401, 403, 405, 501 or 5xx, for every link the account has, each following up to three redirects, inside the request); `jobs/forms.py:307-311` (`apply_logo` calls `logos.from_url` whenever the logo address changes); `jobs/views.py:450-467` (`CompanyLogoActionView.post` sends a `logo` errand) and `jobs/logos.py:297` (`find_on_website`: the company page through `fetching.fetch_page`, then up to six images). None of these calls `throttle`. - **What happens:** an account holder with a few hundred links pressing *Check all* repeatedly, or re-saving companies with changing logo addresses, makes the instance send hundreds of requests per click to hosts of their choosing, from the operator's address, with no ceiling. - **Who can trigger it:** any signed-in account; nothing here is reachable without one. It matters on an instance with more than one person, where one account can spend the operator's reputation and bandwidth on a third party. - **Why it matters:** the capture limit exists to stop exactly this, and the wiki (*Hardening*, *Configuration*) tells operators capture is the only such path, so the bound they rely on is incomplete. - **Fix:** spend a fetch allowance once per outbound URL in `links.check`, in `logos.from_url` and in `CompanyLogoActionView.post` before the errand is sent, either against `POSTULO_CAPTURE_RATE` or against a new `POSTULO_FETCH_RATE` of its own. *Check all* stops when the allowance runs out and says how many links it did not check. Name the new consumers in *Configuration → Rate limits* and *Hardening*. ## Done when - A test in `tests/test_links_and_letters.py` sets the rate to `2/h`, stores three links, posts *Check all*, and asserts only two requests were made and the message says one was not checked. - A test in `tests/test_logos.py` asserts that saving a company with a new logo address and pressing *Find logo* each spend the allowance and are refused with a sentence once it is spent.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:46:28 +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#407
No description provided.