
PoC for CVE-2026-73847 - emlog AI Assistant CSRF to SQL execution to admin takeover (CVSS 6.8)
PoC for missing CSRF protection on emlog pro's AI Assistant execute_tool endpoint, which lets an attacker ride an admin's authenticated session to run arbitrary SQL against the site database — including a full admin account takeover.
| CVE | CVE-2026-73847 |
| CNA | GitHub |
| Advisory | GHSA-v6wr-4x55-7qp5 |
| CVSS 3.1 | 6.8 Medium — AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N |
| CWE | CWE-352 (CSRF), CWE-1275 (Improper SameSite), CWE-798 (Hardcoded write-confirmation string) |
| Affected | emlog pro through 2.6.23 |
| Credit | Dostxodjayev Abdullox (@squeeze440) — pending correction in the CVE record, see below |
emlog pro's admin panel ships an AI assistant that can execute SQL on the admin's behalf via POST /admin/ai.php?action=execute_tool. Several issues stack on this one endpoint:
admin/ calls LoginAuth::checkToken() first (e.g. admin/media.php:140). admin/ai.php never does.admin/ai.php:152, User::isAdmin()) — no Origin/Referer check.include/service/ai.php:594: if (trim($confirm_code) !== 'confirm'). Any forged request just sends confirm_code=confirm.include/service/ai.php:578,589) — a bare authenticated request can SELECT any table.blog table is write-protected (include/service/ai.php:591) — user and every other table are fully writable.password (include/service/ai.php:822-828) — SELECT password AS pwd_hash FROM user returns the raw hash.SameSite attribute (include/lib/loginauth.php:99), leaving Chrome's default "Lax+POST" grace window (roughly the first two minutes after login) as the only thing standing between this and reliable cross-site delivery.Chained together: one forged request from an admin's browser reads every table (including password hashes) and writes to every table except blog, including a direct user.password overwrite.
poc_raw_impact.sh)Isolates the SQL/auth-bypass primitive from the CSRF delivery question. Run against a local instance you control:
./poc_raw_impact.sh http://TARGET admin '<adminpass>'
This logs in as admin, dumps password hashes via column-alias bypass, overwrites the admin password directly through the user table, then logs in again with the attacker-chosen password from a fresh cookie jar — proving full account takeover once one authenticated request reaches the endpoint.

poc_csrf.html)Settles the SameSite question with a live browser rather than assuming it. Serve poc_csrf.html from any origin distinct from the target (a different IP is enough — Chrome treats distinct literal IPs as separate sites) and get a logged-in admin to open it within roughly two minutes of signing in:
python3 -m http.server 8888
# then point poc_csrf.html's form action at your target and get it opened
The form auto-submits on load, POSTing a forged query_database call with confirm_code=confirm cross-site.

Verified live: the forged cross-site POST carried the admin's real auth cookie (sec-fetch-site: cross-site, cookie attached), returned 200 {"code":0,"msg":"ok",...}, and the injected row was confirmed present via a follow-up authenticated read. Repeating the identical request ~48 minutes later against the same, now-aged cookie jar failed — no cookie was attached and the server returned an unauthenticated redirect, confirming the roughly two-minute Lax+POST window is the real constraint (reflected in AC:H).
emlog_options (SMTP creds, API keys, etc.).blog, including user — role/password/email overwrite, demonstrated end-to-end as account takeover.role=admin; writer/editor are blocked by User::checkRolePermission(). Not privilege escalation from a lower role — it turns one malicious link click by a logged-in admin into full, silent site compromise.Add LoginAuth::checkToken() to execute_tool, set SameSite=Strict on the auth cookie, and replace the static confirm_code string with a real per-session, single-use token. Full remediation detail in the advisory.
Note on credit: GitHub-as-CNA published the CVE record without a credits entry, despite the GHSA itself crediting and accepting the reporter. A correction request was sent to [email protected] on 2026-08-16; this README will be updated if the record is corrected.
Published after the advisory was public and a CVE assigned, for defensive/educational use. Do not run this against an emlog instance you don't own or aren't explicitly authorized to test.