mirror of
https://github.com/simonw/datasette.git
synced 2026-06-12 03:57:00 +02:00
Fix SQL injection via bracket escape bypass in escape_sqlite() (#2677)
escape_sqlite() wrapped identifiers in [brackets] without escaping any ] characters inside the string. Since SQLite does not support escaping ] within bracket quoting, an identifier containing ] could break out and inject arbitrary SQL. Fall back to double-quote quoting (doubling any embedded ") when the identifier contains ]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3c1012dcc2
commit
9622662132
2 changed files with 36 additions and 0 deletions
|
|
@ -410,6 +410,10 @@ def escape_css_string(s):
|
|||
def escape_sqlite(s):
|
||||
if _boring_keyword_re.match(s) and (s.lower() not in reserved_words):
|
||||
return s
|
||||
elif "]" in s:
|
||||
# SQLite does not support escaping ] inside [bracket] quoting, so fall
|
||||
# back to double-quote quoting (doubling any embedded ") - #2677
|
||||
return '"{}"'.format(s.replace('"', '""'))
|
||||
else:
|
||||
return f"[{s}]"
|
||||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,38 @@ def test_detect_fts(open_quote, close_quote):
|
|||
conn.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"identifier,expected",
|
||||
(
|
||||
("plain", "plain"),
|
||||
("select", "[select]"),
|
||||
("has space", "[has space]"),
|
||||
("has'quote", "[has'quote]"),
|
||||
# Identifiers containing ] must fall back to double-quote quoting
|
||||
# (SQLite does not support escaping ] inside [brackets]) - #2677
|
||||
("has]bracket", '"has]bracket"'),
|
||||
('has"dquote]', '"has""dquote]"'),
|
||||
),
|
||||
)
|
||||
def test_escape_sqlite(identifier, expected):
|
||||
assert utils.escape_sqlite(identifier) == expected
|
||||
|
||||
|
||||
def test_escape_sqlite_prevents_injection():
|
||||
# https://github.com/simonw/datasette/issues/2677
|
||||
conn = utils.sqlite3.connect(":memory:")
|
||||
conn.execute("CREATE TABLE users (id INTEGER, password TEXT)")
|
||||
conn.execute("INSERT INTO users VALUES (1, 'super_secret_password')")
|
||||
malicious = "users] UNION SELECT password FROM users--"
|
||||
conn.execute('CREATE TABLE "{}" (id INTEGER)'.format(malicious))
|
||||
sql = "select count(*) from {}".format(utils.escape_sqlite(malicious))
|
||||
results = conn.execute(sql).fetchall()
|
||||
conn.close()
|
||||
# The injected UNION must not execute - only the empty malicious table
|
||||
# is queried, so we get a single count row and no leaked password
|
||||
assert results == [(0,)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("table", ("regular", "has'single quote"))
|
||||
def test_detect_fts_different_table_names(table):
|
||||
sql = """
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue