Short description of the issue
Single tracker for nine low-severity findings from a scan of dev over the last 3 months (commits between 2026-02-12 and 2026-05-12). Each is a small bug or behavior change — none are showstoppers, but logging them here so they don't get lost. HEAD 15c749ed.
L1 — sessionCacheLimiter admin-URL detection is prefix-only
Commit: ae20ea03 · File: wire/core/Session/Session.php:574-581
strpos($_SERVER['REQUEST_URI'], $url) === 0 matches URLs like /administrators as admin when the admin URL is /admin. Wrong cache-context header gets selected (perf only, not a security issue).
Fix: boundary check via rtrim($url, '/') . '/'.
L2 — URL sanitizer authority no longer normalized
Commit: 1f8b2726 · File: wire/core/Sanitizer/Sanitizer.php:2362-2375
The encode/decode dance is skipped for the authority portion. URLs with extended characters in userinfo/host that previously round-tripped through FILTER_VALIDATE_URL may now be rejected. Narrow functional regression; no security implication.
L3 — Session::getIP() unsanitized REMOTE_ADDR fallback
Commit: 36e11a60 · File: wire/core/Session/Session.php:1085
When all X-Forwarded-For entries are invalid, falls back to raw $_SERVER['REMOTE_ADDR'] without re-detecting $ipv6 — an IPv6 REMOTE_ADDR can then be sliced with IPv4 logic, producing a malformed partial-IP string in the fingerprint. False session invalidation, not exploitation.
L4 — Paths::short() blindly slices non-matching paths
Commit: bf3d7119 · File: wire/core/Paths.php
substr($value, strlen($root)-1) — when the caller passes a path not under $root, returns a wrong substring silently. Should verify strpos($value, $root) === 0 first and return $value unchanged otherwise.
L5 — WireDataDB::getCache() can clobber existing non-cache meta
Commit: 008ffb5a · File: wire/core/WireDataDB.php
If a meta key contains a non-cache array (no _cre/_exp/_val), decodeCacheValue() returns null and getCache() interprets that as "expired," overwriting the stored value with a cache envelope. Users converting an existing meta key to getCache() silently lose their data.
L6 — FieldtypeComments::getCommentsFields($one=true) index-type change
Commit: 7375d28b (incidental) · File: wire/modules/Fieldtype/FieldtypeComments/FieldtypeComments.module
Internal switch from numerically-indexed to name-indexed array. reset() fixes the $one=true internal path, but any external caller doing $fields[0] on the returned array now gets null.
L7 — PageFinder v2: self::$level not decremented on exception
Commit: 01d6cc00 · Files: wire/core/PageFinder/PageFinder.php:817, wire/core/PageFinder/PageFinder2.php:962
self::$level++ at start of ___find() matched by self::$level-- only on the normal-return paths. Exceptions leave the counter incremented for the rest of the request, suppressing testMode timing.
Fix: wrap body in try { ... } finally { self::$level--; ... }.
L8 — PageFinder v2: getInstance::$settings cache not keyed by Wire instance
Commit: 01d6cc00 · File: wire/core/PageFinder/PageFinder2.php:4084-4087
Static $settings cached on first call; multi-instance ProcessWire setups silently use the first instance's config->PageFinder for all instances. Multi-instance is uncommon but supported.
L9 — status=<published operator translations are counter-intuitive
Commit: abd4bd4b · Files: wire/core/PageFinder/PageFinder.php:701, wire/core/PageFinder/PageFinder2.php:822
The lenient status=published translation also rewrites <, <=, >, >=. status<published becomes status>=unpublished (matches hidden + unpublished + trash), which is the opposite of any reasonable reading. The original ask was leniency on =published typos; the comparison-operator translations weren't part of that. Suggest restricting the table to = and != only.
Setup/Environment
- ProcessWire version:
dev @ 15c749ed
- Scan range: 2026-02-12 → 2026-05-12 (~80 PHP-touching commits)
Short description of the issue
Single tracker for nine low-severity findings from a scan of
devover the last 3 months (commits between 2026-02-12 and 2026-05-12). Each is a small bug or behavior change — none are showstoppers, but logging them here so they don't get lost. HEAD15c749ed.L1 — sessionCacheLimiter admin-URL detection is prefix-only
Commit: ae20ea03 · File:
wire/core/Session/Session.php:574-581strpos($_SERVER['REQUEST_URI'], $url) === 0matches URLs like/administratorsas admin when the admin URL is/admin. Wrong cache-context header gets selected (perf only, not a security issue).Fix: boundary check via
rtrim($url, '/') . '/'.L2 — URL sanitizer authority no longer normalized
Commit: 1f8b2726 · File:
wire/core/Sanitizer/Sanitizer.php:2362-2375The encode/decode dance is skipped for the authority portion. URLs with extended characters in userinfo/host that previously round-tripped through
FILTER_VALIDATE_URLmay now be rejected. Narrow functional regression; no security implication.L3 — Session::getIP() unsanitized REMOTE_ADDR fallback
Commit: 36e11a60 · File:
wire/core/Session/Session.php:1085When all
X-Forwarded-Forentries are invalid, falls back to raw$_SERVER['REMOTE_ADDR']without re-detecting$ipv6— an IPv6 REMOTE_ADDR can then be sliced with IPv4 logic, producing a malformed partial-IP string in the fingerprint. False session invalidation, not exploitation.L4 — Paths::short() blindly slices non-matching paths
Commit: bf3d7119 · File:
wire/core/Paths.phpsubstr($value, strlen($root)-1)— when the caller passes a path not under$root, returns a wrong substring silently. Should verifystrpos($value, $root) === 0first and return$valueunchanged otherwise.L5 — WireDataDB::getCache() can clobber existing non-cache meta
Commit: 008ffb5a · File:
wire/core/WireDataDB.phpIf a meta key contains a non-cache array (no
_cre/_exp/_val),decodeCacheValue()returnsnullandgetCache()interprets that as "expired," overwriting the stored value with a cache envelope. Users converting an existing meta key togetCache()silently lose their data.L6 — FieldtypeComments::getCommentsFields($one=true) index-type change
Commit: 7375d28b (incidental) · File:
wire/modules/Fieldtype/FieldtypeComments/FieldtypeComments.moduleInternal switch from numerically-indexed to name-indexed array.
reset()fixes the$one=trueinternal path, but any external caller doing$fields[0]on the returned array now getsnull.L7 — PageFinder v2: self::$level not decremented on exception
Commit: 01d6cc00 · Files:
wire/core/PageFinder/PageFinder.php:817,wire/core/PageFinder/PageFinder2.php:962self::$level++at start of___find()matched byself::$level--only on the normal-return paths. Exceptions leave the counter incremented for the rest of the request, suppressing testMode timing.Fix: wrap body in
try { ... } finally { self::$level--; ... }.L8 — PageFinder v2: getInstance::$settings cache not keyed by Wire instance
Commit: 01d6cc00 · File:
wire/core/PageFinder/PageFinder2.php:4084-4087Static
$settingscached on first call; multi-instance ProcessWire setups silently use the first instance'sconfig->PageFinderfor all instances. Multi-instance is uncommon but supported.L9 — status=<published operator translations are counter-intuitive
Commit: abd4bd4b · Files:
wire/core/PageFinder/PageFinder.php:701,wire/core/PageFinder/PageFinder2.php:822The lenient
status=publishedtranslation also rewrites<,<=,>,>=.status<publishedbecomesstatus>=unpublished(matches hidden + unpublished + trash), which is the opposite of any reasonable reading. The original ask was leniency on=publishedtypos; the comparison-operator translations weren't part of that. Suggest restricting the table to=and!=only.Setup/Environment
dev@15c749ed