‘ OR 1=1 Still Works in 2026: Pre-Auth SQLi in Moodle (CVE-2026-7274)

On February 13, 2026, I reported a pre-authentication SQL injection in Moodle’s auth_db plugin to the Moodle bug bounty program on Bugcrowd. It was the kind of finding I genuinely did not expect to see in a project this large in 2026: a literal ' OR 1=1-- payload, dropped into the username field on the login page, executes on the backend database. No account needed, no chaining, no clever trick. Triaged as P1, CVSS 9.8. The fix landed on April 29, 2026 as MSA-26-0005 / CVE-2026-7274, with patched releases 5.1.4, 5.0.7, and 4.5.11.

This is a write-up of how a 25-year-old escaping function quietly stayed broken on every database except the one Moodle was tested against.

The plugin

auth_db is one of Moodle’s bundled authentication plugins. It lets an admin point Moodle at an external database (PostgreSQL, MSSQL, Oracle, MySQL, anything ADOdb supports) and authenticate logins against a table in that external DB. It ships in core and is not optional and not third-party.

When a user submits the login form, Moodle’s auth chain eventually calls auth_plugin_db::user_login($username, $password). That function builds a SQL query against the external database to look up the user. The query was assembled like this in auth/db/auth.php:

$rs = $authdb->Execute("SELECT {$this->config->fieldpass}
                        FROM {$this->config->table}
                        WHERE {$this->config->fielduser} = '"
                       .$this->ext_addslashes($extusername)."'");

The defense here is ext_addslashes(). It is defined in the same file:

function ext_addslashes($text) {
  if (empty($this->config->sybasequoting)) {
  $text = str_replace('\\', '\\\\', $text);
  $text = str_replace(array('\'', '"', "\0"),
  array('\\\'', '\\"', '\\0'), $text);
} else {
  $text = str_replace("'", "''", $text);
}
  return $text;
}

If you have written PHP for a while, the default branch will look familiar. It is addslashes() reimplemented inline: turn ' into \', " into \", \ into \\. That is exactly how MySQL escapes string literals.

It is also exactly not how the SQL standard escapes string literals.

Why backslashes are not escaping

In ANSI SQL, backslash has no special meaning inside a string literal. The only way to put a single quote inside a string is to double it, like: ‘O”Brien’. PostgreSQL, MSSQL, Oracle, Firebird, DB2, Informix, and SQLite all follow that rule. MySQL is the outlier: it added backslash escaping as a vendor extension, and since Moodle was historically a MySQL-first project, the escaping function was written for MySQL and never revisited.

Walk the bypass through PostgreSQL with the input ' OR 1=1--:

Input: ' OR 1=1--

Escaped: \' OR 1=1--

SQL: WHERE username = '\' OR 1=1--'

PostgreSQL reads ‘\’ as a string containing a single literal backslash, terminated by the second quote. Everything after that is fresh SQL. The — eats the trailing quote that the template was supposed to close. The query runs, and the escape did nothing.

Of the 31 database driver options listed in the auth_db settings page, 28 are vulnerable on default settings. Only mysql, mysqli, and mysqlt are unaffected, and even those can break under NO_BACKSLASH_ESCAPES.

The same file already used parameterised queries correctly elsewhere, line 609 of the pre-patch source bound $extusername as a parameter in update_user_record(). The four other call sites simply hadn’t been migrated.

The payload

I started up a lab with a vanilla Moodle install with MySQL as the internal DB and PostgreSQL configured as the external auth provider, and tried the following:

Username: ' OR (SELECT pg_sleep(10)) IS NOT NULL--

Password: x

A normal failed login responds in under 200 ms. With the payload, the server hangs for ten seconds before returning the usual “invalid login” page. That is the pg_sleep(10) running on the PostgreSQL backend, executed inside the WHERE clause of an unauthenticated lookup.

From there, everything you would expect from a blind SQLi is on the table: time-based or UNION-based extraction of the entire external auth database. Usernames, password hashes, emails, anything the connection user can read. To make the impact concrete, I wrote a Python script that does exactly that against the live PoC, dumping a table row by row through binary search on pg_sleep. And depending on the backend and the connection user’s privileges, escalation paths like xp_cmdshell are very much in play.

The patch

In commit 2cea70d283 (MDL-88138, “auth_db: drop sybasequoting setting”), every concatenated query was rewritten to use parameter binding:

$rs = $authdb->Execute("SELECT {$this->config->fieldpass}
                        FROM {$this->config->table}
                        WHERE {$this->config->fielduser} = ?", [$extusername]);

Five call sites updated, the sybasequoting configuration option removed, and ext_addslashes() itself marked deprecated with a #[\core\attribute\deprecated] attribute pointing at MDL-88386 for final removal in Moodle 6.0.

Timeline

2026-02-13

Reported via Bugcrowd to the Moodle bug bounty program; triaged as P1

2026-02-18

Bugcrowd marks the submission as Triaged

2026-02-19

Submitted a Python PoC demonstrating data extraction from the external DB

2026-02-24

Moodle picks up the issue

2026-02-27

Moodle reproduces the finding; added to backlog for fix

2026-04-07

Fix committed to main (MDL-88138)

2026-04-29

MSA-26-0005 published

CVE-2026-7274 assigned

Patched releases 5.1.4, 5.0.7, 4.5.11 shipped


Posted

in

by

Tags: