前言
Web安全是每一位开发者都必须掌握的基础知识。无论技术栈如何变化,SQL注入、XSS、CSRF这些经典攻击方式始终是OWASP
Top
10的常客。本文将深入分析各种Web攻击的原理,并给出系统性的防御方案。
攻击类型全景
mindmap
root((Web安全攻防))
注入类
XSS (跨站脚本)
反射型
存储型
DOM型
SQL注入
命令注入
LDAP注入
跨域类
CSRF (跨站请求伪造)
SSRF (服务端请求伪造)
CORS误配置
客户端
点击劫持
开放重定向
WebSocket劫持
认证与会话
Session固定
暴力破解
会话劫持
XSS(跨站脚本攻击)
三种XSS类型
graph TB
subgraph "反射型XSS"
A1[攻击者构造恶意URL] --> A2[用户点击链接]
A2 --> A3[服务端将参数<br>原样反射到页面]
A3 --> A4[恶意脚本在<br>用户浏览器执行]
end
subgraph "存储型XSS"
B1[攻击者提交恶意内容] --> B2[内容存入数据库]
B2 --> B3[其他用户浏览页面]
B3 --> B4[恶意脚本在<br>所有访问者浏览器执行]
end
subgraph "DOM型XSS"
C1[攻击者构造恶意URL] --> C2[前端JS直接读取URL参数]
C2 --> C3[不经过服务端<br>直接插入DOM]
C3 --> C4[恶意脚本执行]
end
XSS攻击示例
反射型XSS场景:搜索页面直接将用户的查询参数输出到HTML中,未做任何转义。攻击者构造包含脚本标签的URL,诱骗用户点击后脚本在用户浏览器中执行。
存储型XSS场景:攻击者在评论区提交包含恶意事件处理器的HTML标签(如img的onerror属性),内容存入数据库后,所有浏览该页面的用户都会触发恶意代码,将Cookie等敏感信息发送到攻击者服务器。
DOM型XSS场景:前端JavaScript直接从URL参数读取内容并写入DOM,不经过服务端处理。
XSS防御策略
1. 输出编码(最基本的防御)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function escapeHTML (str ) { const map = { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' , }; return str.replace (/[&<>"']/g , c => map[c]); }function escapeJS (str ) { return str.replace (/[\\'"<>&]/g , c => { return '\\u' + ('0000' + c.charCodeAt (0 ).toString (16 )).slice (-4 ); }); }function escapeURL (str ) { return encodeURIComponent (str); }
1 2 3 4 5 6 7 8 import "html/template" tmpl := template.Must(template.New("page" ).Parse(` <h1>{{.Title}}</h1> <p>{{.Content}}</p> ` ))
2. Content Security
Policy(CSP)
1 2 3 4 5 6 7 8 9 10 11 12 add_header Content-Security-Policy " default-src 'self'; script-src 'self' 'nonce-${request_id} ' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' https://fonts.googleapis.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; " always;
使用nonce机制时,只有携带正确nonce属性的脚本标签才能被浏览器执行,其他所有内联脚本都会被CSP阻止。
3. DOM型XSS防御
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 element.textContent = userInput;import DOMPurify from 'dompurify' ;const cleanHTML = DOMPurify .sanitize (userInput);if (window .trustedTypes ) { const policy = trustedTypes.createPolicy ('default' , { createHTML : (input ) => DOMPurify .sanitize (input), }); }
CSRF(跨站请求伪造)
攻击原理
sequenceDiagram
participant U as User Browser
participant Bank as bank.com
participant Evil as evil.com
U->>Bank: 1. 登录银行网站
Bank->>U: 2. 设置Session Cookie
U->>Evil: 3. 访问恶意网站
Evil->>U: 4. 返回包含隐藏表单的页面
Note over Evil: 恶意页面包含自动提交的表单<br>目标指向 bank.com/transfer<br>携带攻击者的收款信息
U->>Bank: 5. 浏览器自动携带Cookie提交表单
Note over Bank: 6. 银行无法区分是用户操作还是CSRF
Bank->>Bank: 7. 转账执行!
CSRF防御
1. SameSite Cookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import Flask, make_response app = Flask(__name__)@app.after_request def set_cookie_policy (response ): response.set_cookie( 'session_id' , value='xxx' , samesite='Lax' , secure=True , httponly=True , max_age=3600 ) return response
2. CSRF Token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import secretsfrom functools import wrapsdef generate_csrf_token (): token = secrets.token_hex(32 ) session['csrf_token' ] = token return tokendef csrf_protect (f ): @wraps(f ) def decorated (*args, **kwargs ): if request.method in ('POST' , 'PUT' , 'DELETE' , 'PATCH' ): token = request.form.get('csrf_token' ) or \ request.headers.get('X-CSRF-Token' ) if not token or token != session.get('csrf_token' ): abort(403 , 'CSRF token validation failed' ) return f(*args, **kwargs) return decorated
1 2 3 4 5 6 7 <form method ="POST" action ="/transfer" > <input type ="hidden" name ="csrf_token" value ="{{ csrf_token }}" > <input name ="to" placeholder ="收款人" > <input name ="amount" placeholder ="金额" > <button type ="submit" > 转账</button > </form >
3. Double Submit Cookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function getCookie (name ) { const match = document .cookie .match (new RegExp (`${name} =([^;]+)` )); return match ? match[1 ] : null ; }fetch ('/api/transfer' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-CSRF-Token' : getCookie ('csrf_token' ), }, body : JSON .stringify ({ to : 'user' , amount : 100 }), });
SQL注入
攻击原理
flowchart LR
INPUT["用户输入:<br>恶意SQL片段"] --> QUERY["拼接后的SQL:<br>条件被篡改"]
QUERY --> DB[(Database)]
DB --> RESULT[返回非预期数据!]
style INPUT fill:#d32f2f,color:#fff
style RESULT fill:#d32f2f,color:#fff
当应用程序使用字符串拼接构建SQL查询时,攻击者可以通过在输入中嵌入SQL语法来修改查询的逻辑。例如在用户名字段输入
' OR '1'='1' --
会使WHERE条件永远为真,返回所有记录。更危险的是输入
'; DROP TABLE users; -- 可以直接删除数据表。
SQL注入防御
1. 参数化查询(最有效)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import psycopg2 conn = psycopg2.connect(dsn) cursor = conn.cursor() cursor.execute( "SELECT * FROM users WHERE username = %s AND password = %s" , (username, password_hash) ) cursor.execute( "SELECT * FROM users WHERE username = %(user)s" , {"user" : username} )
1 2 3 4 5 6 7 8 9 db.QueryRow( "SELECT id, name FROM users WHERE email = $1" , email, ) stmt, _ := db.Prepare("INSERT INTO users (name, email) VALUES ($1, $2)" ) stmt.Exec(name, email)
1 2 3 4 5 6 7 PreparedStatement stmt = conn.prepareStatement( "SELECT * FROM users WHERE id = ? AND status = ?" ); stmt.setInt(1 , userId); stmt.setString(2 , "active" );ResultSet rs = stmt.executeQuery();
2. ORM(自带参数化)
1 2 3 4 5 6 7 8 9 10 11 12 13 from sqlalchemy import select, text user = session.execute( select(User).where(User.username == username) ).scalar_one_or_none() result = session.execute( text("SELECT * FROM users WHERE name = :name" ), {"name" : username} )
3. 输入验证
1 2 3 4 5 6 7 8 9 10 11 12 13 def validate_sort_column (column ): """只允许预定义的排序列""" allowed = {'id' , 'name' , 'created_at' , 'updated_at' } if column not in allowed: raise ValueError(f"Invalid sort column: {column} " ) return columndef validate_order (order ): """只允许ASC/DESC""" if order.upper() not in ('ASC' , 'DESC' ): raise ValueError(f"Invalid order: {order} " ) return order.upper()
SSRF(服务端请求伪造)
sequenceDiagram
participant A as Attacker
participant S as Server
participant I as Internal Service<br>(169.254.169.254)
A->>S: POST /api/fetch-url<br>url=http://169.254.169.254/latest/meta-data/
Note over S: 服务端发起请求<br>可以访问内网!
S->>I: GET /latest/meta-data/
I->>S: AWS IAM凭证
S->>A: 返回内网数据
SSRF防御
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import ipaddressfrom urllib.parse import urlparseimport socketclass SSRFProtection : BLOCKED_RANGES = [ ipaddress.ip_network('10.0.0.0/8' ), ipaddress.ip_network('172.16.0.0/12' ), ipaddress.ip_network('192.168.0.0/16' ), ipaddress.ip_network('127.0.0.0/8' ), ipaddress.ip_network('169.254.0.0/16' ), ipaddress.ip_network('::1/128' ), ipaddress.ip_network('fd00::/8' ), ] ALLOWED_SCHEMES = {'http' , 'https' } @classmethod def validate_url (cls, url ): """验证URL是否安全""" parsed = urlparse(url) if parsed.scheme not in cls.ALLOWED_SCHEMES: raise ValueError(f"Scheme not allowed: {parsed.scheme} " ) hostname = parsed.hostname try : ip = ipaddress.ip_address(socket.gethostbyname(hostname)) except (socket.gaierror, ValueError): raise ValueError(f"Cannot resolve: {hostname} " ) for network in cls.BLOCKED_RANGES: if ip in network: raise ValueError(f"Access to internal IP blocked: {ip} " ) port = parsed.port or (443 if parsed.scheme == 'https' else 80 ) if port not in {80 , 443 }: raise ValueError(f"Port not allowed: {port} " ) return url
点击劫持(Clickjacking)
攻击者将目标网站嵌入透明iframe中,诱骗用户点击看似正常的按钮,实际操作的是目标网站。
graph TB
subgraph "用户看到的"
FAKE[看似正常的页面<br>点击领取优惠券]
end
subgraph "实际结构"
VISIBLE[攻击者页面 - 可见层]
IFRAME[目标网站iframe - 透明层<br>opacity: 0<br>包含删除账户按钮]
end
FAKE --> |用户点击| IFRAME
IFRAME --> |实际操作| DELETE[删除账户!]
防御
1 2 3 4 5 6 7 8 9 10 add_header X-Frame-Options "DENY" always;add_header Content-Security-Policy "frame-ancestors 'none'" always;
安全HTTP头汇总
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 server { add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Embedder-Policy "require-corp" always; add_header Cross-Origin-Resource-Policy "same-origin" always; }
安全检查清单
flowchart TB
subgraph "输入处理"
I1[参数化查询 防SQL注入]
I2[输入验证 白名单+长度限制]
I3[输出编码 上下文感知]
end
subgraph "认证授权"
A1[CSRF Token / SameSite Cookie]
A2[密码哈希 bcrypt/argon2]
A3[Session管理 HttpOnly/Secure]
end
subgraph "传输安全"
T1[全站HTTPS + HSTS]
T2[安全Cookie属性]
T3[CORS严格配置]
end
subgraph "防御纵深"
D1[CSP策略]
D2[安全HTTP头]
D3[WAF + 速率限制]
D4[日志与监控]
end
总结
Web安全防御的核心原则:
永远不要信任用户输入 :所有外部数据都必须验证和清理
使用参数化查询 :这是防止SQL注入的最有效手段
输出编码要上下文感知 :HTML、JS、URL、CSS各有不同的编码方式
CSP是XSS的强力防线 :限制脚本来源,防止内联脚本执行
SameSite Cookie + CSRF Token :双重防御CSRF攻击
SSRF防御需要验证目标IP :DNS解析后检查是否为内网地址
纵深防御 :安全头、WAF、日志监控层层保护
安全不是一次性工作,需要持续的代码审计、依赖更新和渗透测试。