Promote the 2026-08-30 review security fixes and supplies ordering to main
This commit is contained in:
85
CHANGELOG.md
85
CHANGELOG.md
@@ -128,6 +128,91 @@ ADR-007 and ADR-002.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Global search answered twelve domains behind one permission.** The route is
|
||||
gated on `assets.view`, and that was the entire check: looking up an asset
|
||||
number also read out of the staff directory, the USB checkout history, the
|
||||
notification log and the network inventory, none of which consult their own
|
||||
permission. The `SEARCH_DOMAINS` settings are site feature toggles, not
|
||||
authorization, and were never doing that job. Each domain now answers only to
|
||||
a caller holding its permission - `employees.view` for the directory,
|
||||
`usb.view` for checkouts, `network.view` for address and subnet matches,
|
||||
`computers.view` for hostnames. Asset, custom-field and vendor/model results
|
||||
stay on `assets.view`, since all three emit asset rows. The check is
|
||||
`caller_holds` rather than `require_permission`: a search that refused the
|
||||
whole request because one domain was off-limits would be useless, so a domain
|
||||
the caller cannot see simply contributes nothing. This is also what makes
|
||||
`assets.view` safe to hand to the guest role again, which is what lets a
|
||||
signed-out visitor look up an asset number without also getting the staff
|
||||
directory.
|
||||
|
||||
- **The printer supplies card was ordered by printer name, so the emptiest
|
||||
cartridge was not first and sometimes was not shown at all.** Rows sorted on
|
||||
"is critical" and then alphabetically, and critical is everything at or below
|
||||
5% - so a cartridge at 5% and one at 0% tied on the first key and the alphabet
|
||||
broke the tie. That is the wrong order on its own, and worse than it looks:
|
||||
the dashboard queue keeps only the first few rows per source, so a printer
|
||||
that was genuinely empty got truncated off the card while a fuller one held
|
||||
its place. The card exists to answer "what do I walk out and change today" and
|
||||
was answering it with the alphabet. Rows now sort by the emptiest cartridge on
|
||||
the printer, ascending. Waste cartridges were already normalised to percent
|
||||
remaining, so one ascending sort is worst-first for every supply type and the
|
||||
critical flag falls out of it rather than needing its own key; the name only
|
||||
breaks ties.
|
||||
|
||||
- **A permission became grantable to anonymous visitors because of how it was
|
||||
spelled.** `guest_grantable_permissions()` opened any permission whose name
|
||||
ended `.view` or `.read` to the guest role, on the reasoning that reading is
|
||||
the safe default. That is true of a permission gating a list of asset numbers
|
||||
and false of one gating bytes, and a suffix cannot tell them apart. Twenty-four
|
||||
of eighty-five qualified, and nobody had decided any of them:
|
||||
`applications.view` reached the SYSTEM-context PowerShell bodies and the
|
||||
installer binaries a site pushes to its bays; `backups.view` returned the full
|
||||
payload that the separate `backups.download` permission exists to protect;
|
||||
`assets.view` gated a global search that queries the staff directory, the USB
|
||||
history and the network inventory with no per-domain check of its own;
|
||||
`audit.view` and `settings.view` are admin-category and were opened by naming
|
||||
alone. Every entry is now DECLARED, by core or by the plugin that owns it,
|
||||
having read what its routes actually return. Adding one is a code change,
|
||||
which is the point - it puts the decision in a diff rather than in a suffix.
|
||||
The two routes serving script bodies and installer packages moved to a new
|
||||
`applications.download` permission, which is not grantable, so the catalogue
|
||||
metadata can still be opened to a site's floor without the bytes going with it.
|
||||
|
||||
- **Deactivating an account did not end its sessions.** There is no token
|
||||
denylist in this product, so the JWT user loader is the only place a
|
||||
deactivated account can be stopped, and it did a bare lookup by id. Somebody
|
||||
deactivated at 09:00 kept a working access token until it expired and a
|
||||
refresh token that minted new ones for thirty days. `/api/auth/refresh` and
|
||||
the API-token path already checked `isactive`; the loader now does too.
|
||||
|
||||
- **The login rate limit could be reset by a header the caller writes.**
|
||||
`client_ip()` read the first `X-Forwarded-For` hop unconditionally, so
|
||||
rotating it per request bought an unbounded budget, and every audit row
|
||||
recorded the forged address. This walked around `TRUSTED_PROXY_COUNT`, which
|
||||
exists to decide whether a proxy is really in front: the app deliberately
|
||||
leaves ProxyFix off by default for exactly this reason, and then read the
|
||||
header anyway. Resolution is now from `remote_addr`, which ProxyFix has
|
||||
already filled from the header where a proxy is configured. Two plugins had
|
||||
hand-rolled the same mistake for their own limiters and now share the helper,
|
||||
which is on `shopdb.api` so the third one does not. **A site behind IIS must
|
||||
set `TRUSTED_PROXY_COUNT`** or its whole floor shares one rate-limit bucket;
|
||||
INSTALL-WINDOWS-IIS.md now says so.
|
||||
|
||||
- **What an operator approved and what an endpoint ran were two different
|
||||
lookups.** Delivery descriptors for `run_script` and `update_app` are resolved
|
||||
when the agent claims a command, not when the command is queued, and the
|
||||
command row recorded no digest - the enqueue path resolved the descriptor
|
||||
purely to validate it and threw the result away. Anyone able to replace a
|
||||
script body or an installer package could do so in the gap, and the
|
||||
re-resolved descriptor would then faithfully describe the new bytes: the agent
|
||||
verifies correctly and runs something nobody approved, with the audit trail
|
||||
naming the operator who queued the original. The approved digest is now pinned
|
||||
on the command at enqueue and the claim refuses a descriptor that no longer
|
||||
matches, failing the push instead of shipping it. Separately, an agent may
|
||||
only finalize a command that is actually running, so a result cannot be posted
|
||||
for a command that was never claimed nor overwrite a recorded failure with a
|
||||
success.
|
||||
|
||||
- **The published site's API documentation page had no spec to show.**
|
||||
`shopdb/core/api/docs.py` reads `docs/openapi.json` at runtime to serve
|
||||
`/api/docs`, and publication excluded the whole of `docs/` because the prose
|
||||
|
||||
Reference in New Issue
Block a user