Back to Blog
critical SEVERITY4 min read

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.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 12, 2026•Reviewed September 12, 2026

Answer Summary

The affected code is the `SQL` class and its `Insert()` and `Update()` methods in u2share_batch_give_sugar, where f-string interpolation was used for database operations. An attacker achieving control over the `value` parameter to `Insert()` or `ucoind`/`pid` parameters to `Update()` could execute arbitrary SQL commands, read the entire SQLite database, modify records, or potentially achieve code execution through SQLite extensions. The fix replaces f-string query construction with parameterized queries using sqlite3's `?` placeholders and tuple parameters. CWE-89 (SQL Injection).

Vulnerability at a Glance

cweCWE-89
fixReplaced f-string SQL construction with sqlite3 parameterized queries using `?` placeholders
riskDatabase compromise, data exfiltration, unauthorized data modification
languagePython
root causeF-string interpolation in SQL query construction instead of parameterized queries
vulnerabilitySQL Injection

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code) — see fix commit
Ecosystem Python (sqlite3 standard library)
CVE / GHSA not assigned
CWE CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

The Vulnerability Explained

The SQL class in this codebase provided a thin wrapper around Python's sqlite3 module, but it undermined the library's built-in protections. Instead of using parameterized queries, the Insert() and Update() methods used Python f-strings to splice values directly into SQL command strings.

Here's the vulnerable Insert() method:

def Insert(self, column, value):
    try:
        sql = f'''INSERT INTO "main"."info" ({column}) VALUES ({value})'''
        logger.debug(sql)
        self.cursor.execute(sql)

And the vulnerable Update() method:

def Update(self, columnvalue, newcolumnvalue):
    try:
        sql = f'UPDATE "main"."info" SET {newcolumnvalue} WHERE {columnvalue}'
        logger.debug(sql)
        self.cursor.execute(sql)

The critical flaw: the execute() call receives a single string argument with values already interpolated, rather than using sqlite3's parameter substitution. Even if current callers passed "safe" values, the API surface itself was poisoned. Any future code path that accepted user input—directly or indirectly—would automatically inherit SQL injection capability without any additional vulnerability being introduced.

Exploitation Path

Consider the Update() method. The parameters columnvalue and newcolumnvalue were string fragments like "pid=123" that were spliced into the WHERE and SET clauses. An attacker controlling either parameter could inject arbitrary SQL:

newcolumnvalue = 'ucoind=100; DROP TABLE info; --'

This would produce:

UPDATE "main"."info" SET ucoind=100; DROP TABLE info; -- WHERE pid=123

The SQLite database—containing user identifiers, currency balances (ucoin, ucoind), and platform IDs (pid)—could be read, modified, or destroyed.

The Fix

The fix replaces f-string construction with sqlite3's parameterized query API. This is not a "sanitization" approach—it's a structural change that separates code from data entirely.

Before (Insert):

def Insert(self, column, value):
    sql = f'''INSERT INTO "main"."info" ({column}) VALUES ({value})'''
    self.cursor.execute(sql)

After (Insert):

def Insert(self, value):
    sql = 'INSERT INTO "main"."info" ("pid", "useractualid", "userid", "ucoin", "ucoind") VALUES (?, ?, ?, ?, ?)'
    self.cursor.execute(sql, value)

Before (Update):

def Update(self, columnvalue, newcolumnvalue):
    sql = f'UPDATE "main"."info" SET {newcolumnvalue} WHERE {columnvalue}'
    self.cursor.execute(sql)

After (Update):

def Update(self, pid, ucoind):
    sql = 'UPDATE "main"."info" SET "ucoind"=? WHERE "pid"=?'
    self.cursor.execute(sql, (ucoind, pid))

The changes are deliberate and API-breaking:

  1. Fixed schema: Insert() no longer accepts arbitrary columns. The five-column schema is hardcoded, preventing column-name injection.
  2. Tuple parameters: Values are passed as a second argument to execute(), not concatenated into the SQL string.
  3. Explicit parameters: Update() now takes pid and ucoind directly, eliminating the string-fragment pattern entirely.

Defense in Depth: XML Parser Hardening

The same commit includes an additional security improvement: the GetUID() function, which parses HTML from an external forum, now uses a hardened XML parser configuration:

xml_parser = etree.HTMLParser(resolve_entities=False, no_network=True)
html = etree.HTML(_html1, parser=xml_parser)

This prevents XML External Entity (XXE) attacks and blocks the parser from making network requests during document parsing—relevant when processing untrusted HTML from forum topics.

Key Takeaways

  • F-strings in SQL are always wrong: Python's f-string formatting has no place in database query construction. The sqlite3 module's parameter substitution (? placeholders with tuple arguments) is the only correct pattern.
  • API design matters for security: The original Insert(column, value) and Update(columnvalue, newcolumnvalue) signatures encouraged callers to build SQL fragments. The fixed signatures force callers to provide data values, not code.
  • "Currently safe" is not "safe": Even if audit showed no external input reached these methods, the vulnerable pattern created persistent risk. Code changes, refactoring, or new features could expose the injection surface without anyone recognizing the danger.
  • Log what you execute, not what you build: The logger.debug(sql) call in the original code logged the fully-interpolated string—potentially exposing sensitive data in logs. The fixed version logs the parameterized query template.
  • Harden parsers processing external data: The resolve_entities=False, no_network=True configuration for etree.HTMLParser demonstrates defense-in-depth for any code handling untrusted markup.

How Orbis AppSec Detected This

Source: The value parameter passed to Insert() and the ucoind/pid parameters passed to Update(), which could originate from any caller of the SQL class methods.

Sink: The cursor.execute() method invoked with a single string argument containing f-string-interpolated SQL, specifically the patterns self.cursor.execute(sql) where sql was constructed via f-string formatting.

Missing control: No use of sqlite3 parameterized queries; the code passed fully-constructed SQL strings rather than using ? placeholders with separate parameter tuples. No validation that column, value, columnvalue, or newcolumnvalue contained only expected characters.

CWE: CWE-89 — Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

Fix: Replaced f-string SQL construction with parameterized queries using sqlite3's ? placeholders and tuple parameters, and hardened the etree.HTMLParser configuration to prevent XXE.

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

This fix demonstrates that SQL injection remains a critical risk even in modern Python code using standard libraries correctly—if those libraries are misused. The sqlite3 module provides robust protection against injection, but only when developers use parameterized queries rather than string formatting. The API redesign of Insert() and Update() shows how security can be built into method signatures, making dangerous patterns structurally impossible. For any codebase maintaining database wrapper classes, this pattern of hardcoded schemas with parameterized execution should be the mandatory standard.

Prevention and further reading

Frequently Asked Questions

Why was the `Insert()` method vulnerable if the `value` parameter appeared to be internally controlled?

The f-string pattern `f'''INSERT INTO "main"."info" ({column}) VALUES ({value})'''` created persistent injection risk. Future code changes, refactoring, or unexpected data flows could expose these parameters to external input without changing the vulnerable query construction pattern.

Does the fix change the method signatures of `Insert()` and `Update()`?

Yes. `Insert()` changed from accepting separate `column` and `value` parameters to accepting a single `value` tuple with five fixed positional elements. `Update()` changed from `columnvalue` and `newcolumnvalue` strings to explicit `pid` and `ucoind` parameters, making the API more restrictive and safer.

What additional security hardening was included in the same fix beyond SQL injection prevention?

The fix also added `xml_parser = etree.HTMLParser(resolve_entities=False, no_network=True)` and updated `GetUID()` to use this hardened parser, preventing XXE and external entity attacks in the HTML parsing path that processes forum topic data.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #6

Related Articles

critical

JdbcSinkFunction.invoke() SQL Injection via Unvalidated Identifiers

The `JdbcSinkFunction.invoke()` method in Lacus's RTC engine built SQL INSERT statements with `String.format()`, placing database name, table name, and column names directly into the query string without validation. Although the actual row values used parameterized `?` placeholders, the identifiers themselves were injectable, letting a crafted sink configuration break out of the intended INSERT statement. The fix adds a strict allowlist regex that rejects any identifier containing characters out

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.

critical

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.

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

redmine_drawio View Hook Inlines Base64 Redmine API Keys

The Redmine drawio plugin's body-bottom view listener embedded the logged-in user's Redmine REST API key into client-side JavaScript on every wiki and issue page, "protected" only by Base64 encoding of the reversed string. Any script on the page — or anyone with browser developer tools, a cached copy of the HTML, or a proxy log — could decode it in one line and act as that user through the Redmine REST API. The fix removes the embedded credential from the rendered page entirely; the plugin's dia