Security
- Null byte rejection middleware — new outermost layer returns 400 Bad Request for URL paths containing
\0,%00, or%2500; previously caused 500/panic in handlers (#565) - Path traversal hardening — additional guards against
../and symlink-based traversal (#560) - Rate limit inversion fix — rate limiter no longer inverts allow/deny logic in certain edge cases (#560)
- javascript: URI injection — metadata links with
javascript:scheme are now stripped (#522, #546) - Reflected XSS in install commands — UI install commands are now HTML-escaped (#521, #545)
- Invalid quarantine/curation/audit mode values rejected — fail-closed on unknown values (#524, #548)
- Credential fields migrated to ProtectedString — secrets zeroed on drop, excluded from Debug (#523, #547)
- Dependency update: tar 0.4.45 → 0.4.46 — fixes PAX header desynchronization (GHSA-3pv8-6f4r-ffg2)
Fixed
- Cargo proxy User-Agent — set
nora/<version>User-Agent on the shared HTTP client; crates.io returns 403 without it (#565) - Docker TOCTOU race — upload session creation now uses atomic file operations (#530, #554)
- Docker blob HEAD check — use
stat()instead of fullget()for HEAD requests (#526, #550) - npm publish with corrupt metadata — reject publish when existing metadata JSON is malformed (#533, #558)
- Terraform serve-stale — serve cached metadata when upstream is unreachable (#532, #557)
- Go Cache-Control — use
is_mutableflag instead ofcontent_typefor header selection (#531, #556) - S3 key roundtrip collision — use
%40encoding for@in S3 storage keys (#534, #559) - GC metadata serialization — serialize metadata cleanup with
publish_lock, makeput()atomic (#529, #553) - StorageBackend::list() — now returns
Resultinstead of panicking on I/O error (#528, #552) - Auth token cache key alignment — insert and lookup use the same key format (#527, #551)
- Auth CIDR prefix=0 overflow — handle arithmetic overflow in TrustedProxies parsing (#525, #549)
- Base URL wildcard host — fail-fast on startup if host is
0.0.0.0withoutNORA_PUBLIC_URL(#510, #512) - Metrics body size_hint — leak detection guard uses
size_hintinstead ofcontent_length(#517, #519)
Changed
- Config refactor —
config.rssplit into per-registry config modules (#484, #564) - AppState Clone —
AppStatenow implementsClonefor AxumFromRefdecomposition (#483, #516) - Proxy fetch newtypes — replaced stringly-typed proxy parameters with newtypes (#482, #515)
- LazyLock migration — replaced
lazy_static!withstd::sync::LazyLock(#373, #480, #514) - LOCK-SAFE annotations — all cache-through proxy functions annotated with lock safety guarantees (#518, #520)
- Rust toolchain pinned to 1.96.0 (#555)
Added
- Playwright E2E contract tests — typed contracts for all 13 registry UI pages, visual regression screenshots (#565)
- 1204 tests (up from 1086 in v0.9.2)
⚠️ Breaking
NORA_PUBLIC_URLrequired whenhost=0.0.0.0— prevents misconfigured URL rewriting. SetNORA_PUBLIC_URL=https://your-domain.comin your environment. (#510, #512)- Dockerfile now sets
NORA_PUBLIC_URL=http://localhost:4000as default. Override with-e NORA_PUBLIC_URL=...for production.
Full Changelog: v0.9.2...v0.9.3