Hardening: re-validate the cached update version before building the package URL #23

Closed
opened 2026-10-16 14:39:58 -07:00 by eric · 0 comments
Owner

Type: hardening (not reachable by a lower-trust actor today). Found by security audit run 4 (commit 77fcd83).

What happens

UpdateChecker::latestVersion() (src/Update/UpdateChecker.php:89-98) returns any string it finds in the multi_domain_redux_latest_version transient. filterUpdate() (:74-84) then puts that string straight into the package and url fields it hands to core's updater.

Today only parseFeed() writes the transient, and it enforces VERSION_REGEX. Anything else that writes the value would bypass that check, for example a direct DB edit, another plugin, or an object-cache collision. It would then control the path segment of the download URL. The origin stays fixed at git.pickysysadmin.ca, which limits what that path control can reach.

Suggested fix

Re-check the cached value in latestVersion():

if ( is_string( $cached ) ) {
	if ( '' === $cached ) {
		return null;
	}
	if ( 1 === preg_match( self::VERSION_REGEX, $cached ) ) {
		return $cached;
	}
	// Invalid cache entry: fall through and refetch.
}

Regression test: a transient holding ../../x/1.0.9 or 9.9.9/evil yields a refetch (or null), never a package URL that contains it.

**Type:** hardening (not reachable by a lower-trust actor today). Found by security audit run 4 (commit `77fcd83`). ## What happens `UpdateChecker::latestVersion()` (`src/Update/UpdateChecker.php:89-98`) returns any string it finds in the `multi_domain_redux_latest_version` transient. `filterUpdate()` (`:74-84`) then puts that string straight into the `package` and `url` fields it hands to core's updater. Today only `parseFeed()` writes the transient, and it enforces `VERSION_REGEX`. Anything else that writes the value would bypass that check, for example a direct DB edit, another plugin, or an object-cache collision. It would then control the path segment of the download URL. The origin stays fixed at `git.pickysysadmin.ca`, which limits what that path control can reach. ## Suggested fix Re-check the cached value in `latestVersion()`: ```php if ( is_string( $cached ) ) { if ( '' === $cached ) { return null; } if ( 1 === preg_match( self::VERSION_REGEX, $cached ) ) { return $cached; } // Invalid cache entry: fall through and refetch. } ``` Regression test: a transient holding `../../x/1.0.9` or `9.9.9/evil` yields a refetch (or `null`), never a package URL that contains it.
eric closed this issue 2026-10-16 15:34:51 -07:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eric/multi-domain-redux#23
No description provided.