Back to Blog
critical SEVERITY5 min read

Toolforge Database Creation SQL Injection via Unicode Backtick Bypass

A critical SQL injection vulnerability in the database creation routine of a Node.js wiki library allowed attackers to inject arbitrary SQL by exploiting a weak validation check. The fix replaces a character-based block with a strict allowlist pattern, ensuring only valid database identifiers reach the SQL engine.

O
By Orbis AppSec
•Published September 30, 2026•Reviewed September 30, 2026

Answer Summary

The create_database function in a Node.js wiki library accepted a dbname parameter that was directly interpolated into SQL queries. An attacker could inject SQL commands by using Unicode variants of backticks (such as U+2018 or U+2019) that passed a simple `includes('`')` check but were interpreted as backticks by MySQL, allowing arbitrary database creation, modification, or data exfiltration. The fix replaces the character-only check with a strict regex pattern `/^[A-Za-z0-9_$]+$/` that only permits ASCII alphanumerics, underscores, and dollar signs. The vulnerability affects all versions prior to the fix; CWE-89 (SQL Injection) applies.

Vulnerability at a Glance

cweCWE-89
fixReplace character-based block with regex allowlist enforcing ASCII alphanumeric, underscore, and dollar-sign identifiers only.
riskAn attacker can execute arbitrary SQL commands, leading to unauthorized database creation, data theft, or service disruption.
languageJavaScript (Node.js)
root causeValidation relied on a single-character check (`includes('`')`) that did not account for Unicode lookalike characters interpreted as SQL metacharacters.
vulnerabilitySQL Injection via insufficient input validation

SQL Injection in Toolforge Database Creation: Unicode Backtick Bypass

A critical SQL injection vulnerability existed in the database creation routine of a Node.js wiki library. The vulnerability allowed attackers to inject arbitrary SQL commands by exploiting a weak input validation check that did not account for Unicode characters that visually or functionally resemble the backtick metacharacter.

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem Node.js
CVE / GHSA not assigned
CWE CWE-89 (SQL Injection)

The Vulnerability Explained

The create_database function accepts a dbname parameter and constructs a SQL query by directly concatenating it into the command string:

if (dbname.includes('`'))
    throw new Error('Invalid database name: [' + dbname + ']');

run_SQL('CREATE DATABASE IF NOT EXISTS `' + dbname + '`', function(

The validation attempts to block backticks by checking if the string contains the ASCII backtick character (U+0060). However, this check is insufficient because MySQL also recognizes Unicode lookalike characters as identifier delimiters in certain parsing contexts.

The Attack

An attacker who controls the dbname parameter can inject SQL by substituting Unicode variants of the backtick:

  • U+2018 (left single quotation mark): '
  • U+2019 (right single quotation mark): '
  • U+300C (left corner bracket, used in CJK text): 「
  • U+300D (right corner bracket): 」

For example, passing dbname = "test'; DROP TABLE users; --" wrapped in U+2018 characters would bypass the includes('')check becauseincludes()` searches only for U+0060. Depending on the MySQL version and character set configuration, the Unicode character may be interpreted as a backtick, allowing the injected SQL to execute:

CREATE DATABASE IF NOT EXISTS `test'; DROP TABLE users; --`

The attacker gains the ability to:
- Create databases under attacker-controlled names
- Execute arbitrary SQL by nesting commands
- Exfiltrate or corrupt data
- Disrupt service availability

This is particularly dangerous in a library distributed to multiple downstream consumers, each of whom passes untrusted user input to create_database without additional validation.

The Fix

The fix replaces the character-based check with a strict regex-based allowlist that explicitly enumerates the only safe characters for a database identifier:

if (!/^[A-Za-z0-9_$]+$/.test(dbname))
    throw new Error('Invalid database name: [' + dbname + ']');

run_SQL('CREATE DATABASE IF NOT EXISTS `' + dbname + '`', function(

How This Solves the Problem

  1. Allowlist instead of blocklist: Rather than attempting to block dangerous characters (which is inherently incomplete), the regex specifies exactly which characters are permitted: ASCII letters, digits, underscores, and dollar signs.

  2. No Unicode lookalikes: The [A-Za-z0-9_$] character class matches only ASCII code points. Unicode variants such as U+2018 will not match and will be rejected.

  3. Anchor to string boundaries: The ^ and $ anchors ensure the entire string matches the pattern, not just a substring. This prevents partial injection.

  4. No escaping ambiguity: By restricting the identifier to safe ASCII characters, the fix eliminates any possibility of the identifier being misinterpreted as SQL syntax, even if the database name is later used in other contexts without quoting.

The regex pattern is more restrictive than MySQL's full identifier rules, but this is intentional—it trades maximum flexibility for maximum safety, which is the correct trade-off for a security boundary.

Key Takeaways

  • Character-based blocking is incomplete: A check for includes('')` only catches the specific ASCII backtick and fails against Unicode lookalikes. Unicode-aware validators must use normalization or explicit character-class ranges.

  • Allowlists are stronger than blocklists for identifiers: For database names, table names, and other identifier-like parameters, specify the exact safe character set rather than trying to enumerate all dangerous ones.

  • Regex anchors matter: Without ^ and $, a pattern like /[A-Za-z0-9_$]/ would match any string containing at least one safe character, still permitting injection. Always anchor identifier validation to the full string.

  • MySQL's backtick is not just ASCII: MySQL recognizes certain Unicode characters as identifier delimiters depending on the server's SQL mode and character set. Do not assume backtick is the only dangerous character in all contexts—use an allowlist.

  • Libraries distribute vulnerability scope: This code runs in downstream consumers' applications. A fix in the library protects all users at once; a bypass affects all of them simultaneously.

How Orbis AppSec Detected This

Source: The dbname parameter passed to the create_database function, which may come from HTTP request data, user input, or API calls.

Sink: The SQL query string constructed via string concatenation and passed to run_SQL().

Missing control: The validation check relied on a single forbidden character (includes('')`) rather than a strict allowlist of permitted characters.

CWE: CWE-89 (SQL Injection) — untrusted input is incorporated into a SQL command without proper escaping or parameterization.

Fix: Replace the character-based block with a regex allowlist pattern that explicitly permits only ASCII alphanumerics, underscores, and dollar signs.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

The Toolforge vulnerability demonstrates a common pitfall: assuming a simple character-based check can secure an identifier field. Unicode lookalike characters and the complexity of SQL parsing make this assumption dangerous. The fix—a strict regex allowlist—is a pattern worth adopting for any situation where user-controlled data becomes part of a SQL identifier, database name, or table name. Allowlists, properly anchored and tested against internationalized input, are far more reliable than blocks.

Prevention and further reading

Frequently Asked Questions

Why does the old check `includes('`')` fail against Unicode backticks like U+2018 (left single quotation mark)?

The `includes()` method searches for the exact character U+0060 (ASCII backtick). Unicode lookalike characters (U+2018, U+2019, U+300C, U+300D) are different code points that pass the check but are still interpreted as backticks by MySQL's parser in certain contexts, allowing injection.

Does the new regex `/^[A-Za-z0-9_$]+$/` match valid MySQL identifier rules?

It is more restrictive than the SQL standard, which allows identifiers up to 64 characters and supports additional characters. However, for a database name allowlist, this restriction is appropriate—it ensures only safe, predictable identifiers are used and eliminates any ambiguity with SQL metacharacters.

Could an attacker bypass the regex by URL-encoding or HTML-encoding the dbname before it reaches create_database?

No. The function receives the decoded parameter value. If encoding layers exist upstream, they decode before reaching this function. The regex validates the actual character codes, not their encoded representation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #44

Related Articles

critical

heatmap.php SQL Injection: $_REQUEST Parameters in Unparameterized

A critical SQL injection vulnerability in the heatmap data retrieval endpoint allowed attackers to execute arbitrary database commands by manipulating coordinate bounds or time range parameters. The vulnerability affected all six user-controlled $_REQUEST parameters passed directly into query construction without parameterization.

critical

Actual Budget addTransaction.sh SQL Injection via Shell Variable

A critical SQL injection vulnerability in Actual Budget's transaction automation script allowed attackers to manipulate database records through shell variables interpolated directly into SQL strings. The fix introduces proper escaping functions and numeric validation to prevent injection through unquoted fields.

critical

SQL's Insert() and Update() Methods Used F-String Interpolation in u2share_batch_give_sugar

The SQL helper class in u2share_batch_give_sugar used Python f-strings to construct INSERT and UPDATE queries, creating SQL injection vulnerabilities even though values appeared to come from internal constants. The fix replaces all f-string query construction with sqlite3 parameterized queries using `?` placeholders, eliminating string interpolation entirely from the database path.

high

TrackOptionsManager DDL: Template-Literal SQL Injection Closed

The `TrackOptionsManager` service built its `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN alias` statements by interpolating a `DEFAULT_ALIAS` constant directly inside single quotes in a JavaScript template literal, and its private `_query()` helper had no parameter channel at all. The fix routes the default value through `mysql.escape()` and gives `_query(q, params = [])` a real bound-parameter argument that is forwarded to `db.query()`. This removes an injection primitive on a schema-bootstra

high

How SQL injection via template literals happens in Node.js SQLite and how to fix it

A SQL injection vulnerability in `src/lib/codex-state.mjs` allowed dynamic column names to reach SQL queries through JavaScript template literals. The fix implements defense-in-depth with strict identifier validation using `SAFE_IDENTIFIER` regex before query construction.

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b