CVE-2026-52771
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.
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
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 High
CVSS: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 High
CVSS: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.phpline 14 -DELETE … WHERE to_tag = '$tag', where$tag = $this->GetPageTag()is again the raw stored tag.handlers/page/deletepage.phplines 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:
- 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, soUserIsOwner($tag)in the delete handler will return true. - 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 usesescape(), which matches the stored quote), andLinkTracker::persist()inserts a row(from_tag='Linker', to_tag='<evil>')into_links— again withescape()on the way in, so the raw quote round-trips. - 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'), runsisOrphaned($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 High
CVSS: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 High
CVSS: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 High
CVSS: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.phpline 14 -DELETE … WHERE to_tag = '$tag', where$tag = $this->GetPageTag()is again the raw stored tag.handlers/page/deletepage.phplines 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:
- 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, soUserIsOwner($tag)in the delete handler will return true. - 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 usesescape(), which matches the stored quote), andLinkTracker::persist()inserts a row(from_tag='Linker', to_tag='<evil>')into_links— again withescape()on the way in, so the raw quote round-trips. - 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'), runsisOrphaned($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 High
CVSS: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
| Package | Affected | Fixed |
|---|---|---|
yeswiki/yeswikiPackagist |
4.2.0 up to 4.6.6 | 4.6.6 |
yeswiki/yeswikiPackagist |
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 lessv4.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
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE 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
github.com/YesWiki/yeswiki/security/advisories/GHSA-8f2v-2qhj-gfwg
github.com/YesWiki/yeswiki/commit/23d3cc124613b9428ab963b31807c08879a9c631
github.com/YesWiki/yeswiki/releases/tag/v4.6.6
github.com/advisories/GHSA-8f2v-2qhj-gfwg
github.com/CVEProject/cvelistV5/tree/main/cves/2026/52xxx/CVE-2026-52771.json
nvd.nist.gov/vuln/detail/CVE-2026-52771
github.com/YesWiki/yeswiki
Left out here: the rest of 2 source descriptions. The JSON record has them.