Python: Move query tests to reflect new file layout

This commit is contained in:
Rasmus Wriedt Larsen
2021-02-16 13:15:01 +01:00
parent 1d6f9bee08
commit 8494fcf45f
114 changed files with 12 additions and 10 deletions

View File

@@ -0,0 +1,3 @@
| BindToAllInterfaces_test.py:5:1:5:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |
| BindToAllInterfaces_test.py:9:1:9:18 | Attribute() | '' binds a socket to all interfaces. |
| BindToAllInterfaces_test.py:17:1:17:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |

View File

@@ -0,0 +1 @@
Security/BadPractice/BindToAllInterfaces/BindToAllInterfaces.ql

View File

@@ -0,0 +1,17 @@
import socket
# binds to all interfaces, insecure
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 31137))
# binds to all interfaces, insecure
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 4040))
# binds only to a dedicated interface, secure
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('84.68.10.12', 8080))
# binds to all interfaces, insecure
ALL_LOCALS = "0.0.0.0"
s.bind((ALL_LOCALS, 9090))

View File

@@ -0,0 +1 @@
semmle-extractor-options: --max-import-depth=3

View File

@@ -0,0 +1,4 @@
| test.py:10:1:10:19 | ControlFlowNode for Attribute() | A Flask app appears to be run in debug mode. This may allow an attacker to run arbitrary code through the debugger. |
| test.py:25:1:25:20 | ControlFlowNode for Attribute() | A Flask app appears to be run in debug mode. This may allow an attacker to run arbitrary code through the debugger. |
| test.py:29:1:29:20 | ControlFlowNode for Attribute() | A Flask app appears to be run in debug mode. This may allow an attacker to run arbitrary code through the debugger. |
| test.py:37:1:37:18 | ControlFlowNode for runapp() | A Flask app appears to be run in debug mode. This may allow an attacker to run arbitrary code through the debugger. |

View File

@@ -0,0 +1 @@
Security/BadPractice/FlaskRunWithDebug/FlaskDebug.ql

View File

@@ -0,0 +1 @@
semmle-extractor-options: --max-import-depth=2 -p ../../lib

View File

@@ -0,0 +1,37 @@
from flask import Flask
app = Flask(__name__)
@app.route('/crash')
def main():
raise Exception()
# bad
app.run(debug=True)
# okay
app.run()
app.run(debug=False)
# also okay
run(debug=True)
app.notrun(debug=True)
# a slightly more involved example using flow and truthy values
DEBUG = True
app.run(debug=DEBUG)
DEBUG = 1
app.run(debug=DEBUG)
if False:
app.run(debug=True)
runapp = app.run
runapp(debug=True)

View File

@@ -0,0 +1,5 @@
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |

View File

@@ -0,0 +1 @@
Security/BadPractice/HTTPSRequestWithoutCertValidation/HTTPSRequestWithoutCertValidation.ql

View File

@@ -0,0 +1,34 @@
import requests
#Simple cases
requests.get('https://semmle.com', verify=True) # GOOD
requests.get('https://semmle.com', verify=False) # BAD
requests.post('https://semmle.com', verify=True) # GOOD
requests.post('https://semmle.com', verify=False) # BAD
# Simple flow
put = requests.put
put('https://semmle.com', verify="/path/to/cert/") # GOOD
put('https://semmle.com', verify=False) # BAD
#Other flow
delete = requests.delete
def req1(verify=False):
delete('https://semmle.com', verify) # BAD
if verify:
delete('https://semmle.com', verify) # GOOD
if not verify:
return
delete('https://semmle.com', verify) # GOOD
patch = requests.patch
def req2(verify):
patch('https://semmle.com', verify=verify) # BAD (from line 30)
req2(False) # BAD (at line 28)
req2("/path/to/cert/") # GOOD
#Falsey value
requests.post('https://semmle.com', verify=0) # BAD

View File

@@ -0,0 +1 @@
semmle-extractor-options: -p ../../lib --max-import-depth=3

View File

@@ -0,0 +1,8 @@
edges
| test.py:5:12:5:24 | hard coded value | test.py:14:18:14:25 | hard coded value |
| test.py:5:12:5:24 | hard coded value | test.py:14:18:14:25 | hard coded value |
| test.py:6:12:6:25 | hard coded value | test.py:15:18:15:25 | hard coded value |
| test.py:6:12:6:25 | hard coded value | test.py:15:18:15:25 | hard coded value |
#select
| test.py:14:18:14:25 | USERNAME | test.py:5:12:5:24 | hard coded value | test.py:14:18:14:25 | hard coded value | Use of $@. | test.py:5:12:5:24 | Str | hardcoded credentials |
| test.py:15:18:15:25 | PASSWORD | test.py:6:12:6:25 | hard coded value | test.py:15:18:15:25 | hard coded value | Use of $@. | test.py:6:12:6:25 | Str | hardcoded credentials |

View File

@@ -0,0 +1 @@
Security/BadPractice/HardcodedCredentials/HardcodedCredentials.ql

View File

@@ -0,0 +1,30 @@
HOST = "acme-trading.com"
PORT = 8000
USERNAME = "road_runner"
PASSWORD = "insecure_pwd"
def sell(client, units):
conn = client.connect(
host=HOST,
port=PORT,
username=USERNAME,
password=PASSWORD)
conn.cmd("sell", 1000)
conn.close()
# Ignored
test(password='short')
test(password='Capitalized')
test(password=' whitespace') # too much whitespace
test(password='insecure__') # too many underscores
test(password='aaaaaaaaaa') # too repetitive
test(password='format_string_{}')
# TODO: we think this is a format string :\
test(password='''U]E8FPETCS_]{,y>bgyzh^$yC5>SP{E*2=`;3]G~k&+;khy3}4]jdpu;D(aP$SCFA{;hh4n46pUJ%+$nEP_gqNq#X!2$%*C-6y6%''')

View File

@@ -0,0 +1 @@
| hosttest.py:6:27:6:51 | Str | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. |

View File

@@ -0,0 +1 @@
Security/BadPractice/IncompleteUrlSanitizer/IncompleteHostnameRegExp.ql

View File

@@ -0,0 +1,2 @@
| urltest.py:9:8:9:30 | Compare | '$@' may be at an arbitrary position in the sanitized URL. | urltest.py:9:8:9:20 | Str | example.com |
| urltest.py:15:8:15:37 | Attribute() | '$@' may be at an arbitrary position in the sanitized URL. | urltest.py:15:24:15:36 | Str | example.com |

View File

@@ -0,0 +1 @@
Security/BadPractice/IncompleteUrlSanitizer/IncompleteUrlSubstringSanitization.ql

View File

@@ -0,0 +1,23 @@
from flask import Flask, request, redirect
import re
app = Flask(__name__)
UNSAFE_REGEX = re.compile("(www|beta).example.com/")
SAFE_REGEX = re.compile(r"(www|beta)\.example\.com/")
@app.route('/some/path/bad')
def unsafe(request):
target = request.args.get('target', '')
if UNSAFE_REGEX.match(target):
return redirect(target)
@app.route('/some/path/good')
def safe(request):
target = request.args.get('target', '')
if SAFE_REGEX.match(target):
return redirect(target)
# FP reported in https://github.com/github/codeql/issues/3712
# This does not define a regex (but could be used by other code to do so)
escaped = re.escape("https://www.humblebundle.com/home/library")

View File

@@ -0,0 +1,50 @@
from flask import Flask, request, redirect
from urllib.parse import urlparse
app = Flask(__name__)
@app.route('/some/path/bad1')
def unsafe1(request):
target = request.args.get('target', '')
if "example.com" in target:
return redirect(target)
@app.route('/some/path/bad2')
def unsafe2(request):
target = request.args.get('target', '')
if target.endswith("example.com"):
return redirect(target)
#Simplest and safest approach is to use an allowlist
@app.route('/some/path/good1')
def safe1(request):
allowlist = [
"example.com/home",
"example.com/login",
]
target = request.args.get('target', '')
if target in allowlist:
return redirect(target)
#More complex example allowing sub-domains.
@app.route('/some/path/good2')
def safe2(request):
target = request.args.get('target', '')
host = urlparse(target).hostname
#Note the '.' preceding example.com
if host and host.endswith(".example.com"):
return redirect(target)
@app.route('/some/path/good3')
def safe3(request):
target = request.args.get('target', '')
target = urlparse(target)
#Start url with https:// and ends with a / so must match the correct domain.
if target and target.startswith("https://example.com/"):
return redirect(target)

View File

@@ -0,0 +1,3 @@
| InsecureTemporaryFile.py:5:16:5:23 | mktemp() | Call to deprecated function tempfile.mktemp may be insecure. |
| InsecureTemporaryFile.py:11:16:11:27 | Attribute() | Call to deprecated function os.tempnam may be insecure. |
| InsecureTemporaryFile.py:17:16:17:26 | Attribute() | Call to deprecated function os.tmpnam may be insecure. |

View File

@@ -0,0 +1,20 @@
from tempfile import mktemp
import os
def write_results1(results):
filename = mktemp()
with open(filename, "w+") as f:
f.write(results)
print("Results written to", filename)
def write_results2(results):
filename = os.tempnam()
with open(filename, "w+") as f:
f.write(results)
print("Results written to", filename)
def write_results3(results):
filename = os.tmpnam()
with open(filename, "w+") as f:
f.write(results)
print("Results written to", filename)

View File

@@ -0,0 +1 @@
Security/BadPractice/InsecureTemporaryFile/InsecureTemporaryFile.ql

View File

@@ -0,0 +1,6 @@
from tempfile import NamedTemporaryFile
def write_results(results):
with NamedTemporaryFile(mode="w+", delete=False) as f:
f.write(results)
print("Results written to", f.name)

View File

@@ -0,0 +1 @@
semmle-extractor-options: -p ../../lib --max-import-depth=3

View File

@@ -0,0 +1,5 @@
| jinja2_escaping.py:9:14:9:39 | ControlFlowNode for Environment() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
| jinja2_escaping.py:41:5:41:29 | ControlFlowNode for Environment() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
| jinja2_escaping.py:43:1:43:3 | ControlFlowNode for E() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
| jinja2_escaping.py:44:1:44:15 | ControlFlowNode for E() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
| jinja2_escaping.py:53:15:53:43 | ControlFlowNode for Template() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |

View File

@@ -0,0 +1 @@
Security/BadPractice/Jinja2RenderWithoutEscape/Jinja2WithoutEscaping.ql

View File

@@ -0,0 +1,55 @@
Environment(loader=templateLoader, autoescape=fake_func())
from flask import Flask, request, make_response, escape
from jinja2 import Environment, select_autoescape, FileSystemLoader, Template
app = Flask(__name__)
loader = FileSystemLoader( searchpath="templates/" )
unsafe_env = Environment(loader=loader)
safe1_env = Environment(loader=loader, autoescape=True)
safe2_env = Environment(loader=loader, autoescape=select_autoescape())
def render_response_from_env(env):
name = request.args.get('name', '')
template = env.get_template('template.html')
return make_response(template.render(name=name))
@app.route('/unsafe')
def unsafe():
return render_response_from_env(unsafe_env)
@app.route('/safe1')
def safe1():
return render_response_from_env(safe1_env)
@app.route('/safe2')
def safe2():
return render_response_from_env(safe2_env)
# Explicit autoescape
e = Environment(
loader=loader,
autoescape=select_autoescape(['html', 'htm', 'xml'])
) # GOOD
# Additional checks with flow.
auto = select_autoescape
e = Environment(autoescape=auto) # GOOD
z = 0
e = Environment(autoescape=z) # BAD
E = Environment
E() # BAD
E(autoescape=z) # BAD
E(autoescape=auto) # GOOD
E(autoescape=0+1) # GOOD
def checked(cond=False):
if cond:
e = Environment(autoescape=cond) # GOOD
unsafe_tmpl = Template('Hello {{ name }}!')
safe1_tmpl = Template('Hello {{ name }}!', autoescape=True)
safe2_tmpl = Template('Hello {{ name }}!', autoescape=select_autoescape())

View File

@@ -0,0 +1 @@
semmle-extractor-options: -p ../lib/ --max-import-depth=3

View File

@@ -0,0 +1,4 @@
| paramiko_host_key.py:5:1:5:49 | ControlFlowNode for Attribute() | Setting missing host key policy to AutoAddPolicy may be unsafe. |
| paramiko_host_key.py:7:1:7:49 | ControlFlowNode for Attribute() | Setting missing host key policy to WarningPolicy may be unsafe. |
| paramiko_host_key.py:11:1:11:51 | ControlFlowNode for Attribute() | Setting missing host key policy to AutoAddPolicy may be unsafe. |
| paramiko_host_key.py:13:1:13:51 | ControlFlowNode for Attribute() | Setting missing host key policy to WarningPolicy may be unsafe. |

View File

@@ -0,0 +1 @@
Security/BadPractice/SSHMissingHostKeyValidation/SSHMissingHostKeyValidation.ql

View File

@@ -0,0 +1 @@
semmle-extractor-options: -p ../../lib --max-import-depth=3

View File

@@ -0,0 +1,13 @@
from paramiko.client import AutoAddPolicy, WarningPolicy, RejectPolicy, SSHClient
client = SSHClient()
client.set_missing_host_key_policy(AutoAddPolicy) # bad
client.set_missing_host_key_policy(RejectPolicy) # good
client.set_missing_host_key_policy(WarningPolicy) # bad
# Using instances
client.set_missing_host_key_policy(AutoAddPolicy()) # bad
client.set_missing_host_key_policy(RejectPolicy()) # good
client.set_missing_host_key_policy(WarningPolicy()) # bad

View File

@@ -0,0 +1,7 @@
| test.py:7:1:7:19 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to world writable. |
| test.py:8:1:8:20 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to world writable. |
| test.py:9:1:9:21 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to world writable. |
| test.py:11:1:11:21 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to group readable. |
| test.py:13:1:13:28 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to group writable. |
| test.py:14:1:14:19 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to group writable. |
| test.py:16:1:16:25 | ControlFlowNode for Attribute() | Overly permissive mask in open sets file to world readable. |

View File

@@ -0,0 +1 @@
Security/BadPractice/WeakFilePermissions/WeakFilePermissions.ql

View File

@@ -0,0 +1 @@
semmle-extractor-options: --max-import-depth=2 -p ../../lib

View File

@@ -0,0 +1,16 @@
import os
import stat
file = 'semmle/important_secrets'
os.chmod(file, 0o7) # BAD
os.chmod(file, 0o77) # BAD
os.chmod(file, 0o777) # BAD
os.chmod(file, 0o600) # GOOD
os.chmod(file, 0o550) # BAD
os.chmod(file, stat.S_IRWXU) # GOOD
os.chmod(file, stat.S_IWGRP) # BAD
os.chmod(file, 400) # BAD -- Decimal format.
os.open(file, 'w', 0o704) # BAD