Skip to content

Commit 4b9c0c3

Browse files
myterongkunzs19110
authored
pySCG Doc2GitHub CWE-707/CWE-89 (#696)
pySCG: adding doc2CWE-89 as part of #531 * removed quoting help(sqllit3) and add XDXC reference * addressed comments, updated xkcd ref and changed formatting * fixed typos --------- Signed-off-by: Helge Wehder <[email protected]> Signed-off-by: myteron <[email protected]> Co-authored-by: Georg Kunz <[email protected]> Co-authored-by: Hubert Daniszewski <[email protected]>
1 parent 40a7c85 commit 4b9c0c3

File tree

5 files changed

+358
-34
lines changed

5 files changed

+358
-34
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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+
![Exploits of a mom](image01.webp "Exploits of a mom")
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|

docs/Secure-Coding-Guide-for-Python/CWE-707/CWE-89/compliant01.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,71 @@
11
# SPDX-FileCopyrightText: OpenSSF project contributors
22
# SPDX-License-Identifier: MIT
33
""" Compliant Code Example """
4+
import logging
45
import sqlite3
6+
from typing import Union
7+
8+
logging.basicConfig(level=logging.DEBUG)
59

610

711
class Records:
8-
""" Compliant code, providing protection against injection.
9-
Missing input sanitation as such.
12+
"""
13+
Compliant code, providing protection against injection.
14+
Missing input sanitation as such.
1015
"""
1116

12-
def __init__(self, data_base: str):
13-
self.connection = sqlite3.connect(data_base)
17+
# TODO: add input sanitation
18+
# TODO: add appropriate logging
19+
# TODO: add appropriate error handling
20+
21+
def __init__(self):
22+
self.connection = sqlite3.connect("school.db")
1423
self.cursor = self.connection.cursor()
1524
self.cursor.execute("CREATE TABLE IF NOT EXISTS Students(student TEXT)")
1625
self.connection.commit()
1726

18-
def get_record(self, name: str) -> list:
19-
'''
27+
def get_record(self, name: str) -> Union[list[tuple[str]], None]:
28+
"""
2029
Fetches a student record from the table for given name
2130
Parameters:
2231
name (string): A string with the student name
2332
Returns:
24-
object (fetchall): sqlite3.cursor.fetchall() object
25-
'''
33+
name (string): A string with the student name
34+
(None): if nothing was found
35+
"""
36+
2637
data_tuple = (name,)
2738
get_values = "SELECT * FROM Students WHERE student = ?"
28-
self.cursor.execute(get_values, data_tuple)
29-
return self.cursor.fetchall()
39+
try:
40+
self.cursor.execute(get_values, data_tuple)
41+
return self.cursor.fetchall()
42+
except sqlite3.OperationalError as operational_error:
43+
logging.warning(operational_error)
44+
return None
3045

3146
def add_record(self, name: str):
32-
'''
47+
"""
3348
Adds a student name to the table
3449
Parameters:
3550
name (string): A string with the student name
36-
'''
51+
"""
3752

3853
data_tuple = (name,)
54+
logging.debug("Adding student %s", name)
3955
add_values = """INSERT INTO Students VALUES (?)"""
4056
try:
4157
self.cursor.execute(add_values, data_tuple)
58+
self.connection.commit()
4259
except sqlite3.OperationalError as operational_error:
43-
print(operational_error)
44-
self.connection.commit()
60+
logging.warning(operational_error)
4561

4662

4763
#####################
4864
# exploiting above code example
4965
#####################
5066
print("sqlite3.sqlite_version=" + sqlite3.sqlite_version)
51-
records = Records("school.db")
5267

68+
records = Records()
5369
records.add_record("Alice")
5470
records.add_record("Robert'); DROP TABLE students;--")
5571
records.add_record("Malorny")
29.1 KB
Loading

0 commit comments

Comments
 (0)