Skip to content

Commit 2b340b4

Browse files
authored
Merge pull request #530 from markshannon/python-no-cert-validation
New query to check for making a request without cert verification.
2 parents a85dfb1 + 698957e commit 2b340b4

File tree

9 files changed

+154
-0
lines changed

9 files changed

+154
-0
lines changed

change-notes/1.19/analysis-python.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ A new predicate `Stmt.getAnEntryNode()` has been added to make it easier to writ
5757
| **Query** | **Tags** | **Purpose** |
5858
|-----------------------------|-----------|--------------------------------------------------------------------|
5959
| Information exposure through an exception (`py/stack-trace-exposure`) | security, external/cwe/cwe-209, external/cwe/cwe-497 | Finds instances where information about an exception may be leaked to an external user. Enabled on LGTM by default. |
60+
| Request without certificate validation (`py/request-without-cert-validation`) | security, external/cwe/cwe-295 | Finds requests where certificate verification has been explicitly turned off, possibly allowing man-in-the-middle attacks. Not enabled on LGTM by default. |
6061

6162
## Changes to existing queries
6263

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Encryption is key to the security of most, if not all, online communication.
9+
Using Transport Layer Security (TLS) can ensure that communication cannot be interrupted by an interloper.
10+
For this reason, is is unwise to disable the verification that TLS provides.
11+
Functions in the <code>requests</code> module provide verification by default, and it is only when
12+
explicitly turned off using <code>verify=False</code> that no verification occurs.
13+
</p>
14+
</overview>
15+
16+
<recommendation>
17+
<p>
18+
Never use <code>verify=False</code> when making a request.
19+
</p>
20+
</recommendation>
21+
22+
<example>
23+
<p>
24+
The example shows two unsafe calls to <a href="https://semmle.com">semmle.com</a>, followed by various safe alternatives.
25+
</p>
26+
27+
<sample src="examples/make_request.py" />
28+
</example>
29+
30+
<references>
31+
<li>
32+
Python requests documentation: <a href="http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification">SSL Cert Verification</a>.
33+
</li>
34+
</references>
35+
</qhelp>
36+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @name Request without certificate validation
3+
* @description Making a request without certificate validation can allow man-in-the-middle attacks.
4+
* @kind problem
5+
* @problem.severity error
6+
* @precision medium
7+
* @id py/request-without-cert-validation
8+
* @tags security
9+
* external/cwe/cwe-295
10+
*/
11+
12+
import python
13+
14+
import semmle.python.web.Http
15+
16+
17+
FunctionObject requestFunction() {
18+
exists(ModuleObject req |
19+
req.getName() = "requests" and
20+
result = req.getAttribute(httpVerbLower())
21+
)
22+
}
23+
24+
/** requests treats None as the default and all other "falsey" values as False */
25+
predicate falseNotNone(Object o) {
26+
o.booleanValue() = false and not o = theNoneObject()
27+
}
28+
29+
from CallNode call, FunctionObject func, Object falsey, ControlFlowNode origin
30+
where
31+
func = requestFunction() and
32+
func.getACall() = call and
33+
falseNotNone(falsey) and
34+
call.getArgByName("verify").refersTo(falsey, origin)
35+
36+
select call, "Call to $@ with verify=$@", func, "requests." + func.getName(), origin, "False"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import requests
2+
3+
#Unsafe requests
4+
5+
requests.get('https://semmle.com', verify=False) # UNSAFE
6+
requests.get('https://semmle.com', verify=0) # UNSAFE
7+
8+
#Various safe options
9+
10+
requests.get('https://semmle.com', verify=True) # Explicitly safe
11+
requests.get('https://semmle.com', verify="/path/to/cert/")
12+
requests.get('https://semmle.com') # The default is to verify.
13+
14+
#Wrapper to ensure safety
15+
16+
def make_safe_request(url, verify_cert):
17+
if not verify_cert:
18+
raise Exception("Trying to make unsafe request")
19+
return requests.get(url, verify_cert)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| make_request.py:5:1:5:48 | ControlFlowNode for Attribute() | Call to $@ with verify=$@ | ../lib/requests.py:2:1:2:36 | Function get | requests.get | make_request.py:5:43:5:47 | ControlFlowNode for False | False |
2+
| make_request.py:7:1:7:49 | ControlFlowNode for Attribute() | Call to $@ with verify=$@ | ../lib/requests.py:11:1:11:46 | Function post | requests.post | make_request.py:7:44:7:48 | ControlFlowNode for False | False |
3+
| make_request.py:12:1:12:39 | ControlFlowNode for put() | Call to $@ with verify=$@ | ../lib/requests.py:14:1:14:34 | Function put | requests.put | make_request.py:12:34:12:38 | ControlFlowNode for False | False |
4+
| make_request.py:28:5:28:46 | ControlFlowNode for patch() | Call to $@ with verify=$@ | ../lib/requests.py:17:1:17:36 | Function patch | requests.patch | make_request.py:30:6:30:10 | ControlFlowNode for False | False |
5+
| make_request.py:34:1:34:45 | ControlFlowNode for Attribute() | Call to $@ with verify=$@ | ../lib/requests.py:11:1:11:46 | Function post | requests.post | make_request.py:34:44:34:44 | ControlFlowNode for IntegerLiteral | False |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-295/RequestWithoutValidation.ql
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import requests
2+
3+
#Simple cases
4+
requests.get('https://semmle.com', verify=True) # GOOD
5+
requests.get('https://semmle.com', verify=False) # BAD
6+
requests.post('https://semmle.com', verify=True) # GOOD
7+
requests.post('https://semmle.com', verify=False) # BAD
8+
9+
# Simple flow
10+
put = requests.put
11+
put('https://semmle.com', verify="/path/to/cert/") # GOOD
12+
put('https://semmle.com', verify=False) # BAD
13+
14+
#Other flow
15+
delete = requests.delete
16+
17+
def req1(verify=False):
18+
delete('https://semmle.com', verify) # BAD
19+
if verify:
20+
delete('https://semmle.com', verify) # GOOD
21+
if not verify:
22+
return
23+
delete('https://semmle.com', verify) # GOOD
24+
25+
patch = requests.patch
26+
27+
def req2(verify):
28+
patch('https://semmle.com', verify=verify) # BAD (from line 30)
29+
30+
req2(False) # BAD (at line 28)
31+
req2("/path/to/cert/") # GOOD
32+
33+
#Falsey value
34+
requests.post('https://semmle.com', verify=0) # BAD
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
semmle-extractor-options: -p ../lib/ --max-import-depth=3
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
def get(url, params=None, **kwargs):
3+
pass
4+
5+
def options(url, **kwargs):
6+
pass
7+
8+
def head(url, **kwargs):
9+
pass
10+
11+
def post(url, data=None, json=None, **kwargs):
12+
pass
13+
14+
def put(url, data=None, **kwargs):
15+
pass
16+
17+
def patch(url, data=None, **kwargs):
18+
pass
19+
20+
def delete(url, **kwargs):
21+
pass

0 commit comments

Comments
 (0)