Files
whatsapp/find_mixed_params.py
T
2026-02-21 07:37:36 -05:00

141 lines
5.8 KiB
Python

#!/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.")