PST-053: The GeoNames download and the update check are read without a size bound, and GeoNames has no integrity check #782
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#782
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-053. 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/jobs/management/commands/fetch_geonames.py(82-96);src/postulo/core/updates.py(47-55; 52-55)Who can do it, and what they need. Whoever can make download.geonames.org answer differently: a compromise of that host, its DNS plus a CA-valid certificate, or a hostile mirror if an operator ever redirects the URL. The request itself goes through public_only_client with certificate verification, so a plain on-path attacker cannot do this.
How it works.
manage.py fetch_geonameson first start when the table is absent (docker/entrypoint.sh:31-35).client.get(url)reads the whole response body into memory with no limit (every other fetch in the code base uses http.read_body(limit=...)).zipfile.ZipFile(io.BytesIO(response.content))thenarchive.read(name)inflates the single member fully in memory, so a small zip bomb becomes gigabytes.|| echo), so the effect is a killed provisioning step and a map without dots; alternatively a tampered but shape-valid table (>=10000 rows, the five required cities present) is accepted and placed on the operator's map.Impact. Memory exhaustion of the provisioning command at first start, or silent substitution of the city/country table that drives company placement. Low impact because the data only feeds map coordinates and the request has nobody's data in it.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Keep it low. Precondition: control of the answers from download.geonames.org (host compromise, or a CA-valid on-path attacker); nothing a user or stranger can trigger. Effect: memory exhaustion of the first-start
fetch_geonamescommand, retried on every start while the table is absent. The container does not stop, because the entrypoint continues past the failure. The other effect is a shape-valid table that puts companies at wrong coordinates. No user data is involved, no markup is rendered from the table, and the other effects are not confidentiality or integrity of any account's records. Lines are correct (83, 88, 91). Remediation: stream the response with http.read_body(limit=...) and a deadline, cap the inflated size by reading the zip member in bounded pieces, and optionally pin a checksum or compare against the previous table.Suggested remediation. Read the body through postulo.plugins.http.read_body with a ceiling (a few tens of MB), check ZipInfo.file_size against a ceiling before archive.read, and either pin a published checksum per image build or ship the dataset with the image/release so start-up fetches nothing.
Also covered by this issue.
src/postulo/core/updates.py(47-55); Low). A daily memory spike or crash of the scheduler container driven by a remote release feed. Narrow and opt-in; recorded for completeness because the project's own rule is to bound every download.src/postulo/core/updates.py(52-55); Informational). Availability of the scheduler or a worker, from a source the operator chose to trust. No data exposure. Listed because the project's own standard (#321) is that a stranger's body is read streamed and capped, and these two callers of the capped clients bypass the cap.Fixed in
7a881151don main.