#!/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'(? 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'(?