PST-031: The API route table and allowed methods are disclosed to unauthenticated callers through 405 answers #760
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#760
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 in the security audit of 2026-10-10 (
SECURITY-AUDIT-2026-10-10.md), run againstmainatc30851bfrom 8 to 10 October 2026, as PST-031. Severity: Low.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.
src/postulo/api/api.py(883-927; 854-901)Who can do it, and what they need. A stranger on the internet with no token. Precondition: none, only network reach to the instance.
How it works.
DELETE /api/v1/applicationsorPOST /api/v1/me, with no Authorization header.Allowheader._no_such_addressinstead.Allow: GET, POST) against 401 the attacker enumerates every API address and its methods without a token, andGET /api/v1/returns Django's HTML 404 page rather than a 401. Verified:client.delete('/api/v1/applications')-> 405Allow: GET, POSTapplication/problem+json;client.head(...)-> 405;client.get('/api/v1/does-not-exist')-> 401;client.get('/api/v1/')-> 404 text/html.Impact. Fingerprints a Postulo instance and its full route table to anyone, contradicting the threat model's 'the API answers 401 to everything without a live token' and the stated purpose of
_no_such_address('so the route table is not probed without one'). No data is exposed.Evidence.
Verification (verdict: confirmed; severity after verification: Low). Low (hardening gap versus a stated project claim). Attacker is an unauthenticated network peer; effect is route-table/method enumeration and API fingerprinting only (the route table is public in the open-source repo). Precise lines: api.py:875-901 (wrapper), api.py:904-916 (_no_such_address), ninja operation.py:673-677 and ninja/openapi/views.py:13-16 (HTML 404 at the API root). Remediation: answer 401 for a request lacking a live bearer before any 405 (e.g. in the wrapper, run lookup() and return the 401 problem when there is no live token), and give the root path a 401/problem response instead of ninja's default_home 404.
Suggested remediation. In
_answer_a_wrong_method_as_a_problem, when the wrapped view returns 405, apply the same token check_no_such_addressmakes and answer 401 (withoutAllow) when no live token is presented; alternatively run the method check after authentication. Put_no_such_address(or a 401) in front of ninja's rootdefault_homepattern soGET /api/v1/is also a 401 problem document.Also covered by this issue.
src/postulo/api/api.py(854-901); Low). Contradicts the threat model's promise that 'the API answers 401 to everything without a live token' and the stated purpose of _no_such_address (#430: 'so the route table is not probed without one'). …Fixed in
b4fff603aon main.