Every CVE Search/ Get API key

CVE-2026-52771

YesWiki: Second-Order SQL Injection in Page Delete API via Unescaped Page Tag (`ApiController::deletePage`)

8.3 HighFix available

YesWiki is a wiki system written in PHP. From version 4.2.0 to before version 4.6.6, ApiController::deletePage() interpolates a page tag retrieved from the database into a DELETE FROM …_links WHERE to_tag = '$tag' query without escaping. The page tag is attacker-controlled — the POST /api/pages/{tag} API accepts arbitrary URL-encoded values, including single quotes, and stores them. A low-privilege authenticated user can therefore create a page whose tag is a SQL fragment, make the page non-orphaned via the standard {{include page="…"}} link mechanism, and then invoke the delete endpoint to execute arbitrary SQL inside the wiki database - including time-based blind data exfiltration from any table. This issue has been patched in version 4.6.6.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
Low
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
Low

Exploit likelihood

0.51%Up 0.5 points since Jun 14, 2026
90%50%10%1%

What each source reports

CVE List Score used 8.3

YesWiki: Second-Order SQL Injection in Page Delete API via Unescaped Page Tag (`ApiController::deletePage`)

YesWiki is a wiki system written in PHP. From version 4.2.0 to before version 4.6.6, ApiController::deletePage() interpolates a page tag retrieved from the database into a DELETE FROM …_links WHERE to_tag = '$tag' query without escaping. The page tag is attacker-controlled — the POST /api/pages/{tag} API accepts arbitrary URL-encoded values, including single quotes, and stores them. A low-privilege authenticated user can therefore create a page whose tag is a SQL fragment, make the page non-orphaned via the standard {{include page="…"}} link mechanism, and then invoke the delete endpoint to execute arbitrary SQL inside the wiki database - including time-based blind data exfiltration from any table. This issue has been patched in version 4.6.6.

Record
CVE-2026-52771
CVSS 3.1
8.3 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
Weakness
CWE-89
Updated
2026-11-02
EUVDEUVD-2026-71803 8.3

YesWiki is a wiki system written in PHP. From version 4.2.0 to before version 4.6.6, ApiController::deletePage() interpolates a page tag retrieved from the database into a DELETE FROM …_links WHERE to_tag = '$tag' query without escaping. The page tag is attacker-controlled — the POST /api/pages/{tag} API accepts arbitrary URL-encoded values, including single quotes, and stores them. A low-privilege authenticated user can therefore create a page whose tag is a SQL fragment, make the page non-orphaned via the standard {{include page="…"}} link mechanism, and then invoke the delete endpoint to execute arbitrary SQL inside the wiki database - including time-based blind data exfiltration from any table. This issue has been patched in version 4.6.6.

Record
EUVD-2026-71803
CVSS 3.1
8.3 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
Updated
2026-11-02
GitHub AdvisoriesGHSA-8f2v-2qhj-gfwg 8.3

YesWiki: Second-Order SQL Injection in Page Delete API via Unescaped Page Tag (`ApiController::deletePage`)

Summary

ApiController::deletePage() interpolates a page tag retrieved from the database into a DELETE FROM …_links WHERE to_tag = '$tag' query without escaping. The page tag is attacker-controlled — the POST /api/pages/{tag} API accepts arbitrary URL-encoded values, including single quotes, and stores them. A low-privilege authenticated user can therefore create a page whose tag is a SQL fragment, make the page non-orphaned via the standard {{include page="…"}} link mechanism, and then invoke the delete endpoint to execute arbitrary SQL inside the wiki database - including time-based blind data exfiltration from any table.

This is a classic second-order SQL injection: the INSERT correctly escapes the value, so the malicious tag is stored intact and the input passes every "is this value safe to put in the database?" check; the sink is the read-back-and-reuse path, where escaping is omitted.

Details

Affected component

  • File: includes/controllers/ApiController.php
  • Method: ApiController::deletePage($tag)
  • Route: @Route("/api/pages/{tag}", methods={"DELETE"}, options={"acl":{"+"}}) — acl:"+" means any authenticated user.
  • Sink: line 626
// includes/controllers/ApiController.php  (v4.6.5 = origin/doryphore-dev HEAD,
// lines 607–631)
public function deletePage($tag)
{
    $pageManager   = $this->getService(PageManager::class);
    $pageController = $this->getService(PageController::class);
    $dbService     = $this->getService(DbService::class);
    ...
    try {
        $page = $pageManager->getOne($tag, null, false);     // (a) safe SELECT
        if (empty($page)) { ... } else {
            $tag = isset($page['tag']) ? $page['tag'] : $tag;//   ^ raw tag from DB
            $result['notDeleted'] = [$tag];
            if ($this->wiki->UserIsOwner($tag) || $this->wiki->UserIsAdmin()) {
                if (!$pageManager->isOrphaned($tag)) {
                    $dbService->query(
                        "DELETE FROM {$dbService->prefixTable('links')}
                         WHERE to_tag = '$tag'");           // (b) SINK — unescaped
                }
                ...

The same anti-pattern shows up in two adjacent files; both were noted in the original submission and confirmed during validation:

  • tools/tags/handlers/page/__deletepage.php line 14 - DELETE … WHERE to_tag = '$tag', where $tag = $this->GetPageTag() is again the raw stored tag.
  • handlers/page/deletepage.php lines 93–94 - LoadAll('SELECT DISTINCT from_tag FROM …links WHERE to_tag = '" . $this->GetPageTag() . "'"), same pattern as a SELECT instead of a DELETE.

The API path is the easiest sink to reach because it requires only acl:"+" and a single HTTP request; the other two require a logged-in user to navigate to the page's delete handler

A low-privilege account can carry the whole chain:

  1. Plant — POST /api/pages/{evil} with body=anything. PageManager::save() escapes the tag at INSERT time ('\'' in SQL ⇒ stored '), so the tag persists with its single quote intact. The new page is owned by the attacker, so UserIsOwner($tag) in the delete handler will return true.
  2. Make non-orphaned — save any second page whose body contains {{include page="<evil>"}} through the web edit handler. LinkTracker::preventTrackingActions() parses the include directive, looks up the referenced page (PageManager::getOne() finds it because lookup uses escape(), which matches the stored quote), and LinkTracker::persist() inserts a row (from_tag='Linker', to_tag='<evil>') into _links — again with escape() on the way in, so the raw quote round-trips.
  3. Trigger — DELETE /api/pages/{evil}. The delete handler reads the page (escaped SELECT, finds the row), assigns $tag = $page['tag'] (the raw stored value, including '), runs isOrphaned($tag) (escaped SELECT, returns not orphaned because step 2 inserted a row), and then runs the…
Record
GHSA-8f2v-2qhj-gfwg
CVSS 3.1
8.3 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
Weakness
CWE-89
Updated
2026-11-02
NVD 8.3

YesWiki is a wiki system written in PHP. From version 4.2.0 to before version 4.6.6, ApiController::deletePage() interpolates a page tag retrieved from the database into a DELETE FROM …_links WHERE to_tag = '$tag' query without escaping. The page tag is attacker-controlled — the POST /api/pages/{tag} API accepts arbitrary URL-encoded values, including single quotes, and stores them. A low-privilege authenticated user can therefore create a page whose tag is a SQL fragment, make the page non-orphaned via the standard {{include page="…"}} link mechanism, and then invoke the delete endpoint to execute arbitrary SQL inside the wiki database - including time-based blind data exfiltration from any table. This issue has been patched in version 4.6.6.

Record
CVE-2026-52771
CVSS 3.1
8.3 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
Weakness
CWE-89
Updated
2026-11-02
OSV 8.3

YesWiki: Second-Order SQL Injection in Page Delete API via Unescaped Page Tag (`ApiController::deletePage`)

YesWiki is a wiki system written in PHP. From version 4.2.0 to before version 4.6.6, ApiController::deletePage() interpolates a page tag retrieved from the database into a DELETE FROM …_links WHERE to_tag = '$tag' query without escaping. The page tag is attacker-controlled — the POST /api/pages/{tag} API accepts arbitrary URL-encoded values, including single quotes, and stores them. A low-privilege authenticated user can therefore create a page whose tag is a SQL fragment, make the page non-orphaned via the standard {{include page="…"}} link mechanism, and then invoke the delete endpoint to execute arbitrary SQL inside the wiki database - including time-based blind data exfiltration from any table. This issue has been patched in version 4.6.6.

Record
CVE-2026-52771
CVSS 3.1
8.3 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
Weakness
CWE-89
Updated
2026-11-02
OSVGHSA-8f2v-2qhj-gfwg 8.3

YesWiki: Second-Order SQL Injection in Page Delete API via Unescaped Page Tag (`ApiController::deletePage`)

Summary

ApiController::deletePage() interpolates a page tag retrieved from the database into a DELETE FROM …_links WHERE to_tag = '$tag' query without escaping. The page tag is attacker-controlled — the POST /api/pages/{tag} API accepts arbitrary URL-encoded values, including single quotes, and stores them. A low-privilege authenticated user can therefore create a page whose tag is a SQL fragment, make the page non-orphaned via the standard {{include page="…"}} link mechanism, and then invoke the delete endpoint to execute arbitrary SQL inside the wiki database - including time-based blind data exfiltration from any table.

This is a classic second-order SQL injection: the INSERT correctly escapes the value, so the malicious tag is stored intact and the input passes every "is this value safe to put in the database?" check; the sink is the read-back-and-reuse path, where escaping is omitted.

Details

Affected component

  • File: includes/controllers/ApiController.php
  • Method: ApiController::deletePage($tag)
  • Route: @Route("/api/pages/{tag}", methods={"DELETE"}, options={"acl":{"+"}}) — acl:"+" means any authenticated user.
  • Sink: line 626
// includes/controllers/ApiController.php  (v4.6.5 = origin/doryphore-dev HEAD,
// lines 607–631)
public function deletePage($tag)
{
    $pageManager   = $this->getService(PageManager::class);
    $pageController = $this->getService(PageController::class);
    $dbService     = $this->getService(DbService::class);
    ...
    try {
        $page = $pageManager->getOne($tag, null, false);     // (a) safe SELECT
        if (empty($page)) { ... } else {
            $tag = isset($page['tag']) ? $page['tag'] : $tag;//   ^ raw tag from DB
            $result['notDeleted'] = [$tag];
            if ($this->wiki->UserIsOwner($tag) || $this->wiki->UserIsAdmin()) {
                if (!$pageManager->isOrphaned($tag)) {
                    $dbService->query(
                        "DELETE FROM {$dbService->prefixTable('links')}
                         WHERE to_tag = '$tag'");           // (b) SINK — unescaped
                }
                ...

The same anti-pattern shows up in two adjacent files; both were noted in the original submission and confirmed during validation:

  • tools/tags/handlers/page/__deletepage.php line 14 - DELETE … WHERE to_tag = '$tag', where $tag = $this->GetPageTag() is again the raw stored tag.
  • handlers/page/deletepage.php lines 93–94 - LoadAll('SELECT DISTINCT from_tag FROM …links WHERE to_tag = '" . $this->GetPageTag() . "'"), same pattern as a SELECT instead of a DELETE.

The API path is the easiest sink to reach because it requires only acl:"+" and a single HTTP request; the other two require a logged-in user to navigate to the page's delete handler

A low-privilege account can carry the whole chain:

  1. Plant — POST /api/pages/{evil} with body=anything. PageManager::save() escapes the tag at INSERT time ('\'' in SQL ⇒ stored '), so the tag persists with its single quote intact. The new page is owned by the attacker, so UserIsOwner($tag) in the delete handler will return true.
  2. Make non-orphaned — save any second page whose body contains {{include page="<evil>"}} through the web edit handler. LinkTracker::preventTrackingActions() parses the include directive, looks up the referenced page (PageManager::getOne() finds it because lookup uses escape(), which matches the stored quote), and LinkTracker::persist() inserts a row (from_tag='Linker', to_tag='<evil>') into _links — again with escape() on the way in, so the raw quote round-trips.
  3. Trigger — DELETE /api/pages/{evil}. The delete handler reads the page (escaped SELECT, finds the row), assigns $tag = $page['tag'] (the raw stored value, including '), runs isOrphaned($tag) (escaped SELECT, returns not orphaned because step 2 inserted a row), and then runs the…
Record
GHSA-8f2v-2qhj-gfwg
CVSS 3.1
8.3 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
Weakness
CWE-89
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
yeswiki/yeswiki
Packagist
4.2.0 up to 4.6.6
4.6.6
yeswiki/yeswiki
Packagist
4.2.0 up to 4.6.6, plus listed versions
4.2.3v4.2.0v4.2.1v4.2.2v4.2.4
Show 20 moreShow less
v4.3v4.3.1v4.4.0v4.4.1v4.4.2v4.4.3v4.4.4v4.4.5v4.5.0v4.5.1v4.5.2v4.5.3v4.5.4v4.5.5v4.6.0v4.6.1v4.6.2v4.6.3v4.6.4v4.6.5
4.6.6

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-52771 is a high vulnerability (CVSS 8.3) in Yeswiki, published July 9, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.51%, the 44th percentile. Fixed in yeswiki/yeswiki 4.6.6.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open