|
| 1 | +# CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') |
| 2 | + |
| 3 | +To prevent SQL injections, use input sanitization and parameterized queries instead of `executescript()`. |
| 4 | + |
| 5 | +Combining an SQL query with data from lesser trusted sources, such as fields from a web form used to sign up new clients, can allow an attacker to inject malicious SQL statements. |
| 6 | + |
| 7 | +SQLs that allow running operating system commands as part of an SQL query are sensitive to Remote Code Execution (RCE) attacks such as `\system` or `\!`, in *MySQL Shell commands* [[Oracle 2024]](https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-commands.html). SQL injections have been known for decades *ODBC and MS SQL server 6.5* [[Jeff Forristal 1998]](http://phrack.org/issues/54/8.html#article) and are still part of the most frequent top 10 attacks in CWE top 25 [[MITRE 2024]](https://cwe.mitre.org/top25/archive/2024/2024_cwe_top25.html) and 2021 [[OWASP 2021]](https://owasp.org/www-project-top-ten/). |
| 8 | + |
| 9 | +Note that "Raw SQL is error prone, more labor intensive, and ugly." [Byrne 2021] |
| 10 | + |
| 11 | +Expoits of a Mom [XKCD 2007](https://xkcd.com/327/) |
| 12 | + |
| 13 | + |
| 14 | +Suppose a school has a webpage that allows parents to add the name of their child to a school trip themselves. The webpage is using an SQL back-end adding a new student `<NAME>` as follows: |
| 15 | + |
| 16 | +```SQL |
| 17 | +INSERT INTO Students(student) VALUES('<NAME>') |
| 18 | +``` |
| 19 | + |
| 20 | +Suppose an attacker can substitute arbitrary strings for <NAME> and is allowed to extend it with any character |
| 21 | + |
| 22 | +```SQL |
| 23 | +<NAME>'); DROP TABLE students;--") |
| 24 | +``` |
| 25 | +
|
| 26 | +The attacker can then end and close the query himself with '); and extend the query with other SQL commands. |
| 27 | + |
| 28 | +```SQL |
| 29 | +INSERT INTO Students(student) VALUES('Robert'); DROP TABLE students;--'); |
| 30 | +``` |
| 31 | + |
| 32 | +`DROP TABLE students;` will delete the existing table with student names. |
| 33 | + |
| 34 | +The `executescript()` method is typically used to initialize, create or do any back-end work without front-end interaction and not intended to be used with a data from a lesser trusted source. |
| 35 | + |
| 36 | +## Non-compliant Code Example - SQLite3 |
| 37 | + |
| 38 | +The `noncompliant01.py` code allows parents to add their Child by mixing the name with required SQL in an f-string and running both in `executescript()`. |
| 39 | + |
| 40 | +[*noncompliant01.py:*](noncompliant01.py) |
| 41 | + |
| 42 | +```python |
| 43 | +# SPDX-FileCopyrightText: OpenSSF project contributors |
| 44 | +# SPDX-License-Identifier: MIT |
| 45 | +""" Non-compliant Code Example """ |
| 46 | +import logging |
| 47 | +import sqlite3 |
| 48 | +from typing import Union |
| 49 | +
|
| 50 | +logging.basicConfig(level=logging.DEBUG) |
| 51 | +
|
| 52 | +
|
| 53 | +class Records: |
| 54 | + """ |
| 55 | + Non-compliant code. |
| 56 | + Possible SQL injection vector through string-based query construction |
| 57 | + """ |
| 58 | +
|
| 59 | + def __init__(self): |
| 60 | + self.connection = sqlite3.connect("school.db") |
| 61 | + self.cursor = self.connection.cursor() |
| 62 | + self.cursor.execute("CREATE TABLE IF NOT EXISTS Students(student TEXT)") |
| 63 | + self.connection.commit() |
| 64 | +
|
| 65 | + def get_record(self, name: str = "") -> Union[list[tuple[str]], None]: |
| 66 | + """ |
| 67 | + Fetches a student record from the table for given name |
| 68 | + Parameters: |
| 69 | + name (string): A string with the student name |
| 70 | + Returns: |
| 71 | + name (string): A string with the student name |
| 72 | + (None): if nothing was found |
| 73 | + """ |
| 74 | +
|
| 75 | + get_values = f"SELECT * FROM Students WHERE student = '{name}'" |
| 76 | + try: |
| 77 | + self.cursor.execute(get_values) |
| 78 | + return self.cursor.fetchall()[0][0] |
| 79 | + except sqlite3.OperationalError as operational_error: |
| 80 | + logging.error(operational_error) |
| 81 | + return None |
| 82 | +
|
| 83 | + def add_record(self, name: str = ""): |
| 84 | + """ |
| 85 | + Adds a student name to the table |
| 86 | + Parameters: |
| 87 | + name (string): A string with the student name |
| 88 | + """ |
| 89 | +
|
| 90 | + add_values = "INSERT INTO Students(student) VALUES('{name}');" |
| 91 | + logging.debug("Adding student %s", name) |
| 92 | + add_values_query = add_values.format(name=name) |
| 93 | + try: |
| 94 | + self.cursor.executescript(add_values_query) |
| 95 | + self.connection.commit() |
| 96 | + except sqlite3.OperationalError as operational_error: |
| 97 | + logging.error(operational_error) |
| 98 | +
|
| 99 | +
|
| 100 | +##################### |
| 101 | +# exploiting above code example |
| 102 | +##################### |
| 103 | +print("sqlite3.sqlite_version=" + sqlite3.sqlite_version) |
| 104 | +
|
| 105 | +records = Records() |
| 106 | +records.add_record("Alice") |
| 107 | +records.add_record("Robert'); DROP TABLE students;--") |
| 108 | +records.add_record("Malorny") |
| 109 | +
|
| 110 | +print(records.get_record("Alice")) |
| 111 | +# normal as "Robert" has not been added as "Robert": |
| 112 | +print(records.get_record("Robert'); DROP TABLE students;--")) |
| 113 | +print(records.get_record("Malorny")) |
| 114 | +
|
| 115 | +``` |
| 116 | + |
| 117 | +Adding a student name called `'); DROP TABLE students;--"` allows to drop/delete the table or run any other SQL command. |
| 118 | + |
| 119 | +**Example `noncompliant01.py` output:** |
| 120 | + |
| 121 | +```bash |
| 122 | +sqlite3.sqlite_version=3.34.1 |
| 123 | +DEBUG:root:Adding student Alice |
| 124 | +DEBUG:root:Adding student Robert'); DROP TABLE students;-- |
| 125 | +DEBUG:root:Adding student Malorny |
| 126 | +ERROR:root:no such table: Students |
| 127 | +ERROR:root:no such table: Students |
| 128 | +None |
| 129 | +ERROR:root:near ")": syntax error |
| 130 | +None |
| 131 | +ERROR:root:no such table: Students |
| 132 | +None |
| 133 | +``` |
| 134 | + |
| 135 | +Student `"Malorny"` will never be added because the table `Students` is dropped by the time her parents want to add her. |
| 136 | + |
| 137 | +Static code analysis tools such as `bandit 1.7.4` can be used on the command line to discover or gatekeep vulnerable code anywhere in a product life-cycle: |
| 138 | + |
| 139 | +**Example bandit 1.7.4 output:*** |
| 140 | + |
| 141 | +```bash |
| 142 | +Test results: |
| 143 | +>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction. |
| 144 | + Severity: Medium Confidence: Low |
| 145 | + CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html) |
| 146 | + Location: .\noncompliant01.py:24:21 |
| 147 | + More Info: https://bandit.readthedocs.io/en/1.7.4/plugins/b608_hardcoded_sql_expressions.html |
| 148 | +23 ''' |
| 149 | +24 get_values = "SELECT * FROM Students WHERE student = '{name}'".format(name=name) |
| 150 | +25 self.cursor.execute(get_values) |
| 151 | +``` |
| 152 | + |
| 153 | +## Compliant Solution - sqlite3 |
| 154 | + |
| 155 | +The `compliant01.py` code example is using `sqlite3.cursor.execute(get_values, data_tuple)` that: |
| 156 | + |
| 157 | +* Separates the query from the values to reduce injection attacks. |
| 158 | + |
| 159 | +```python |
| 160 | +data_tuple = (name,)`<br> |
| 161 | +get_values = "SELECT * FROM Students WHERE student = ?" |
| 162 | +self.cursor.execute(get_values, data_tuple) |
| 163 | +``` |
| 164 | + |
| 165 | +* Is limited to a single-line query to protect against multi-line attacks. |
| 166 | + |
| 167 | +The `compliant01.py` code is also providing variable type hints in its methods such as `name: str`. The `add_student` method is now storing the whole length of the string `"Robert'); DROP TABLE students;--"`. Input sanitation as described in separate rules would have to be added. |
| 168 | + |
| 169 | +>[!NOTE] |
| 170 | +> |
| 171 | +> * Type hints do not prevent simple string injections at runtime. They only help prevent coding mistakes when used with a special linter at design time. |
| 172 | +> * The `sqlite3.cursor.executescript()` method is specifically designed to prohibit printing the output. That is to prevent an attacker from exploring the database back-end layout. |
| 173 | +> * Production code must use logging that avoids exposing sensitive data. |
| 174 | +> * Input sanitation as described in separate rules would have to be added. |
| 175 | +
|
| 176 | +[*compliant01.py:*](compliant01.py) |
| 177 | + |
| 178 | +```python |
| 179 | +# SPDX-FileCopyrightText: OpenSSF project contributors |
| 180 | +# SPDX-License-Identifier: MIT |
| 181 | +""" Compliant Code Example """ |
| 182 | +import logging |
| 183 | +import sqlite3 |
| 184 | +from typing import Union |
| 185 | + |
| 186 | +logging.basicConfig(level=logging.DEBUG) |
| 187 | + |
| 188 | + |
| 189 | +class Records: |
| 190 | + """ |
| 191 | + Compliant code, providing protection against injection. |
| 192 | + Missing input sanitation as such. |
| 193 | + """ |
| 194 | + |
| 195 | + # TODO: add input sanitation |
| 196 | + # TODO: add appropriate logging |
| 197 | + # TODO: add appropriate error handling |
| 198 | + |
| 199 | + def __init__(self): |
| 200 | + self.connection = sqlite3.connect("school.db") |
| 201 | + self.cursor = self.connection.cursor() |
| 202 | + self.cursor.execute("CREATE TABLE IF NOT EXISTS Students(student TEXT)") |
| 203 | + self.connection.commit() |
| 204 | + |
| 205 | + def get_record(self, name: str) -> Union[list[tuple[str]], None]: |
| 206 | + """ |
| 207 | + Fetches a student record from the table for given name |
| 208 | + Parameters: |
| 209 | + name (string): A string with the student name |
| 210 | + Returns: |
| 211 | + name (string): A string with the student name |
| 212 | + (None): if nothing was found |
| 213 | + """ |
| 214 | + |
| 215 | + data_tuple = (name,) |
| 216 | + get_values = "SELECT * FROM Students WHERE student = ?" |
| 217 | + try: |
| 218 | + self.cursor.execute(get_values, data_tuple) |
| 219 | + return self.cursor.fetchall() |
| 220 | + except sqlite3.OperationalError as operational_error: |
| 221 | + logging.warning(operational_error) |
| 222 | + return None |
| 223 | + |
| 224 | + def add_record(self, name: str): |
| 225 | + """ |
| 226 | + Adds a student name to the table |
| 227 | + Parameters: |
| 228 | + name (string): A string with the student name |
| 229 | + """ |
| 230 | + |
| 231 | + data_tuple = (name,) |
| 232 | + logging.debug("Adding student %s", name) |
| 233 | + add_values = """INSERT INTO Students VALUES (?)""" |
| 234 | + try: |
| 235 | + self.cursor.execute(add_values, data_tuple) |
| 236 | + self.connection.commit() |
| 237 | + except sqlite3.OperationalError as operational_error: |
| 238 | + logging.warning(operational_error) |
| 239 | + |
| 240 | + |
| 241 | +##################### |
| 242 | +# exploiting above code example |
| 243 | +##################### |
| 244 | +print("sqlite3.sqlite_version=" + sqlite3.sqlite_version) |
| 245 | + |
| 246 | +records = Records() |
| 247 | +records.add_record("Alice") |
| 248 | +records.add_record("Robert'); DROP TABLE students;--") |
| 249 | +records.add_record("Malorny") |
| 250 | + |
| 251 | +print(records.get_record("Alice")) |
| 252 | +# normal as "Robert" has not been added as "Robert": |
| 253 | +print(records.get_record("Robert'); DROP TABLE students;--")) |
| 254 | +print(records.get_record("Malorny")) |
| 255 | +``` |
| 256 | + |
| 257 | +**Example `compliant01.py` output:** |
| 258 | + |
| 259 | +```bash |
| 260 | +sqlite3.sqlite_version=3.34.1 |
| 261 | +DEBUG:root:Adding student Alice |
| 262 | +DEBUG:root:Adding student Robert'); DROP TABLE students;-- |
| 263 | +DEBUG:root:Adding student Malorny |
| 264 | +[('Alice',)] |
| 265 | +[("Robert'); DROP TABLE students;--",)] |
| 266 | +[('Malorny',)] |
| 267 | +``` |
| 268 | +
|
| 269 | +## Automated Detection |
| 270 | +
|
| 271 | +|Tool|Version|Checker|Description| |
| 272 | +|:---|:---|:---|:---| |
| 273 | +|Bandit|1.7.3 on python 3.9.6|[B608](https://bandit.readthedocs.io/en/1.7.4/plugins/b608_hardcoded_sql_expressions.html)|bandit can detect a full SQL string assigned to a variable such as<br> `get_values = "SELECT * FROM Students WHERE student = '{name}'".format(name=name)`<br><br>bandit is unable to detect the same query assigned over multiple lines:<br>`get_values = "SELECT * FROM Students WHERE student = "`<br>`get_values += "'{name}'".format(name=name)`| |
| 274 | +
|
| 275 | +## Related Guidelines |
| 276 | +
|
| 277 | +||| |
| 278 | +|:---|:---| |
| 279 | +|[MITRE CWE](http://cwe.mitre.org/)|Pillar: [CWE-707: Improper Neutralization (4.12)](https://cwe.mitre.org/data/definitions/707.html)| |
| 280 | +|[MITRE CWE](http://cwe.mitre.org/)|Base [CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')](https://cwe.mitre.org/data/definitions/89.html)| |
| 281 | +|[SEI CERT Coding Standard for Java](https://wiki.sei.cmu.edu/confluence/display/java/SEI+CERT+Oracle+Coding+Standard+for+Java)|[IDS00-J. Prevent SQL injection](https://wiki.sei.cmu.edu/confluence/display/java/IDS00-J.+Prevent+SQL+injection)| |
| 282 | +|[SEI CERT C Coding Standard](https://web.archive.org/web/20220511061752/https://wiki.sei.cmu.edu/confluence/display/c/SEI+CERT+C+Coding+Standard)|[STR02-C. Sanitize data passed to complex subsystems](https://wiki.sei.cmu.edu/confluence/display/c/STR02-C.+Sanitize+data+passed+to+complex+subsystems)| |
| 283 | +|[SEI CERT C++ Coding Standard](https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=88046682)|[VOID STR02-CPP. Sanitize data passed to complex subsystems](https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=88046726)| |
| 284 | +|[SEI CERT Perl Coding Standard](https://wiki.sei.cmu.edu/confluence/display/perl/SEI+CERT+Perl+Coding+Standard)|[IDS33-PL. Sanitize untrusted data passed across a trust boundary](https://wiki.sei.cmu.edu/confluence/display/perl/IDS33-PL.+Sanitize+untrusted+data+passed+across+a+trust+boundary)| |
| 285 | +|[[OWASP 2005](https://wiki.sei.cmu.edu/confluence/display/java/Rule+AA.+References#RuleAA.References-OWASP05)]|[A Guide to Building Secure Web Applications and Web Services](http://sourceforge.net/projects/owasp/files/Guide/2.0.1/OWASPGuide2.0.1.pdf/download)| |
| 286 | +
|
| 287 | +## Bibliography |
| 288 | +
|
| 289 | +||| |
| 290 | +|:---|:---| |
| 291 | +|[XKCD 2007]|327 Exploits of a Mom, Available [online] from: [https://xkcd.com/327/](https://xkcd.com/327/), [Accessed 2024] | |
| 292 | +|[Jeff Forristal 1998]|Phrack magazine. Batch commands in ODBC and MS SQL server 6.5, Available [online] from: [http://phrack.org/issues/54/8.html#article](http://phrack.org/issues/54/8.html#article ) [accessed 11 November 2024] | |
| 293 | +|[Oracle 2024]|Oracle MySQL Documentation. MySQL Shell commands, Available [online] from: [https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-commands.html](https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-commands.html]), [Accessed Nov 2024]| |
| 294 | +|[OWASP 2005]|A Guide to Building Secure Web Applications and Web Services, Available from [http://sourceforge.net/projects/owasp/files/Guide/2.0.1/OWASPGuide2.0.1.pdf/download](http://sourceforge.net/projects/owasp/files/Guide/2.0.1/OWASPGuide2.0.1.pdf/download), [Accessed Nov 2024]| |
| 295 | +|[MITRE 2024]|Top 25 2024, available from [https://cwe.mitre.org/top25/archive/2024/2024_cwe_top25.html](https://cwe.mitre.org/top25/archive/2024/2024_cwe_top25.html), [Accessed Nov 2024]| |
| 296 | +|[Byrne 2021]|Byrne, D. (2021), Full Stack Python Security, ISBN 9781617298820, Manning, page 205| |
0 commit comments