up
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find SQL queries mixing named and positional parameters in PHP files."""
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
|
||||
dirs = ['api/', 'classes/', 'services/']
|
||||
results = []
|
||||
|
||||
for d in dirs:
|
||||
for filepath in glob.glob(os.path.join(d, '**', '*.php'), recursive=True):
|
||||
try:
|
||||
with open(filepath, 'r', errors='replace') as f:
|
||||
content = f.read()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
lines = content.split('\n')
|
||||
total_lines = len(lines)
|
||||
|
||||
# APPROACH 1: Look at the whole file content for SQL strings
|
||||
# Find all SQL string assignments and prepare/query calls
|
||||
# Use regex to find multi-line strings between quotes
|
||||
|
||||
# Find all quoted strings that look like SQL (may span lines with concatenation)
|
||||
# Pattern: capture everything between matching quotes in SQL context
|
||||
|
||||
# First find all $var = "..." or prepare("...") blocks
|
||||
# Including multi-line with string concatenation
|
||||
|
||||
# Simpler approach: for each file, find all occurrences of execute([...])
|
||||
# and check if the SQL and params are mixed
|
||||
|
||||
# APPROACH: Read entire file, find SQL strings, check for mixed params
|
||||
# Look for patterns like: "SELECT ... ? ... :param ..."
|
||||
|
||||
# Find all string literals that contain SQL keywords
|
||||
# Handle both single-line and multi-line concatenated strings
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Skip comment lines
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('//') or stripped.startswith('*') or stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
# Look for SQL query assignments or prepare/query calls on this line
|
||||
is_sql_line = bool(
|
||||
re.search(r'(prepare|query|exec)\s*\(', line, re.IGNORECASE) or
|
||||
re.search(r'\$\w*(sql|query)\w*\s*=', line, re.IGNORECASE)
|
||||
)
|
||||
|
||||
if not is_sql_line:
|
||||
continue
|
||||
|
||||
# Gather block: from this line until statement seems complete
|
||||
block_lines_list = []
|
||||
j = i
|
||||
brace_depth = 0
|
||||
while j < total_lines and j < i + 50:
|
||||
bl = lines[j]
|
||||
block_lines_list.append(bl)
|
||||
brace_depth += bl.count('(') - bl.count(')')
|
||||
# End conditions
|
||||
if j > i and brace_depth <= 0 and ';' in bl:
|
||||
break
|
||||
j += 1
|
||||
|
||||
block = '\n'.join(block_lines_list)
|
||||
|
||||
# Now extract ALL string content from this block
|
||||
# Handle concatenated strings like "part1" . "part2"
|
||||
# and "part1
|
||||
# part2" (multi-line strings)
|
||||
all_strings = []
|
||||
# Double-quoted strings (handle escaped quotes)
|
||||
all_strings.extend(re.findall(r'"((?:[^"\\]|\\.)*)"', block))
|
||||
# Single-quoted strings
|
||||
all_strings.extend(re.findall(r"'((?:[^'\\]|\\.)*)'", block))
|
||||
|
||||
full_sql = ' '.join(all_strings)
|
||||
|
||||
# Must contain SQL keywords
|
||||
if not re.search(r'\b(SELECT|INSERT|UPDATE|DELETE|REPLACE\s+INTO)\b', full_sql, re.IGNORECASE):
|
||||
continue
|
||||
|
||||
# Check for positional (?) - but not in ternary context
|
||||
# In SQL strings, ? should appear as a standalone placeholder
|
||||
has_positional = bool(re.search(r'\?', full_sql))
|
||||
|
||||
# Check for named params - :word but not :: or :// or :\
|
||||
named_in_sql = re.findall(r'(?<![:\w/]):[a-zA-Z_][a-zA-Z0-9_]*', full_sql)
|
||||
# Filter out things that are clearly not SQL params (like :hover, :root from CSS)
|
||||
named_in_sql = [n for n in named_in_sql if n.lower() not in (':hover', ':root', ':focus', ':active', ':visited')]
|
||||
has_named = len(named_in_sql) > 0
|
||||
|
||||
if has_positional and has_named:
|
||||
# Extra validation: make sure ? is in the SQL part, not just in error messages etc.
|
||||
# Check each individual string for SQL content
|
||||
sql_strings = [s for s in all_strings if re.search(r'\b(SELECT|INSERT|UPDATE|DELETE|WHERE|SET|FROM|INTO|VALUES)\b', s, re.IGNORECASE)]
|
||||
combined_sql = ' '.join(sql_strings)
|
||||
|
||||
has_pos_in_sql = bool(re.search(r'\?', combined_sql))
|
||||
named_in_real_sql = re.findall(r'(?<![:\w/]):[a-zA-Z_][a-zA-Z0-9_]*', combined_sql)
|
||||
named_in_real_sql = [n for n in named_in_real_sql if n.lower() not in (':hover', ':root', ':focus', ':active', ':visited')]
|
||||
|
||||
if has_pos_in_sql and named_in_real_sql:
|
||||
results.append({
|
||||
'file': filepath,
|
||||
'start_line': i + 1,
|
||||
'end_line': i + len(block_lines_list),
|
||||
'named_params': named_in_real_sql,
|
||||
'block': block.strip(),
|
||||
'sql_text': combined_sql[:500]
|
||||
})
|
||||
|
||||
# Deduplicate (same file + same start line)
|
||||
seen = set()
|
||||
unique_results = []
|
||||
for r in results:
|
||||
key = (r['file'], r['start_line'])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_results.append(r)
|
||||
|
||||
# Output results
|
||||
if not unique_results:
|
||||
print("No mixed parameter queries found.")
|
||||
else:
|
||||
for r in unique_results:
|
||||
print("=" * 80)
|
||||
print(f"FILE: {r['file']}")
|
||||
print(f"LINES: {r['start_line']}-{r['end_line']}")
|
||||
print(f"NAMED PARAMS: {r['named_params']}")
|
||||
print(f"SQL TEXT: {r['sql_text'][:400]}")
|
||||
print(f"CODE BLOCK:")
|
||||
for bl in r['block'].split('\n'):
|
||||
print(f" {bl}")
|
||||
print()
|
||||
|
||||
print(f"TOTAL: {len(unique_results)} potential mixed parameter queries found.")
|
||||
Reference in New Issue
Block a user