SEC2-018: The logo-fetch allowance is charged to the company's owner, and to a shared "None" bucket for group-owned companies #960
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#960
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-10-16 in a read-only security review of
postuloat56fc9d531, checked by a second, independent pass.Severity: Low · Category: Business Logic · Reference: CWE-840 (Business Logic Errors); CWE-770 (Allocation of Resources Without Limits or Throttling); OWASP A04:2021 (Insecure Design)
Location:
src/postulo/jobs/logos.py:267-282(spend_fetch(company.owner)infrom_url)src/postulo/jobs/forms.py:434-439(CompanyForm.apply_logocallslogos.from_url(company, url)with the defaultspend=True)src/postulo/jobs/views.py:1108-1115, 1269-1275(LogoFormMixin.form_valid,CompanyLogoActionView._set),:1230(Find logo, which spends forrequest.user)src/postulo/core/throttle.py:114-117(_key:identity = getattr(who, "pk", None) or str(who))Issue
logos.from_urlspends the fetch allowance ofcompany.owner, which before #896 was alwaysthe requester. Now a company may be owned by a group (
ownerisNone) or edited by aread/write grantee:
whoisNone, so_keyyields one key shared by everylogo-address fetch on every group company of the instance, belonging to nobody.
CompanyUpdateView,access = WRITE), theowner's allowance is spent by someone else. In
CompanyLogoActionView._setthe requester'sallowance is spent first (
throttle.fetch(request.user)) and then the owner's a second time.Impact
A read/write grantee can exhaust the owner's fetch allowance (
POSTULO_FETCH_RATE), whichalso gates link checks on the owner's CV, without the owner doing anything; on an instance
with group-owned companies, any member can exhaust the shared bucket for every group. The rate
limit was introduced by #407 to bound server-side fetches of person-chosen addresses; charging
the wrong account weakens it rather than bypasses it.
Fix
Spend the allowance on the person who pressed the button: give
from_urlabyargument(
spend_fetch(by)) and passself.userfromCompanyForm.apply_logo.CompanyLogoActionView._setand the Find logo view already spend for
request.user, so they callfrom_urlwithspend=False, which also ends the double charge. Done whentests/test_logos.py(ortests/security/test_rate_limits.py) asserts that a grantee's logo fetch on a shared companyincrements the grantee's counter once and the owner's not at all, and that a group-owned
company's fetch is counted against the requester.
Related
#407 (closed) - introduced the fetch allowance, before sharing. #896 (closed) - introduced group ownership and grantee edits; silent on throttles.