Security · #web-security#xss#csrf#sql-injection

Web安全攻防:XSS/CSRF/SQL注入防护

2025.06.29 8 min 3.2k
// 目录 · contents

前言

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
// HTML上下文 - HTML实体编码
function escapeHTML(str) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
};
return str.replace(/[&<>"']/g, c => map[c]);
}

// JavaScript上下文 - JS编码
function escapeJS(str) {
return str.replace(/[\\'"<>&]/g, c => {
return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
});
}

// URL上下文 - URL编码
function escapeURL(str) {
return encodeURIComponent(str);
}
1
2
3
4
5
6
7
8
// Go模板自动转义
import "html/template"

tmpl := template.Must(template.New("page").Parse(`
<h1>{{.Title}}</h1>
<p>{{.Content}}</p>
`))
// {{.Title}} 和 {{.Content}} 自动HTML转义

2. Content Security Policy(CSP)

1
2
3
4
5
6
7
8
9
10
11
12
# Nginx CSP配置
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
// 不安全: 直接使用DOM API插入未净化的HTML
// 永远不要将用户输入直接作为HTML插入DOM

// 安全: 使用textContent(自动转义)
element.textContent = userInput;

// 安全: 使用DOMPurify清理必须渲染的HTML
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(userInput);

// 安全: 使用Trusted Types API
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 设置SameSite Cookie属性
from flask import Flask, make_response

app = Flask(__name__)

@app.after_request
def set_cookie_policy(response):
# Strict: 完全禁止第三方Cookie
# Lax: 允许GET导航携带Cookie(默认值)
# None: 允许跨站(必须配合Secure)
response.set_cookie(
'session_id',
value='xxx',
samesite='Lax', # 推荐Lax
secure=True, # HTTPS only
httponly=True, # JS无法访问
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
# 服务端生成和验证CSRF Token
import secrets
from functools import wraps

def generate_csrf_token():
token = secrets.token_hex(32)
session['csrf_token'] = token
return token

def 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
<!-- 表单中嵌入CSRF Token -->
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 前端: 从Cookie读取token并放入请求头
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
# Python - 参数化查询
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
// Go - 参数化查询
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
// Java - PreparedStatement
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
# SQLAlchemy ORM
from sqlalchemy import select, text

# 安全: ORM自动参数化
user = session.execute(
select(User).where(User.username == username)
).scalar_one_or_none()

# 如果必须使用raw SQL,使用text()绑定参数
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 column

def 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 ipaddress
from urllib.parse import urlparse
import socket

class SSRFProtection:
# 禁止访问的IP范围
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'), # AWS metadata
ipaddress.ip_network('::1/128'),
ipaddress.ip_network('fd00::/8'),
]

ALLOWED_SCHEMES = {'http', 'https'}

@classmethod
def validate_url(cls, url):
"""验证URL是否安全"""
parsed = urlparse(url)

# 1. 协议白名单
if parsed.scheme not in cls.ALLOWED_SCHEMES:
raise ValueError(f"Scheme not allowed: {parsed.scheme}")

# 2. DNS解析并检查IP
hostname = parsed.hostname
try:
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
except (socket.gaierror, ValueError):
raise ValueError(f"Cannot resolve: {hostname}")

# 3. 检查IP是否在禁止范围内
for network in cls.BLOCKED_RANGES:
if ip in network:
raise ValueError(f"Access to internal IP blocked: {ip}")

# 4. 端口白名单
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
# 方案1: X-Frame-Options
add_header X-Frame-Options "DENY" always;
# DENY: 不允许任何嵌入
# SAMEORIGIN: 只允许同源嵌入

# 方案2: CSP frame-ancestors (更灵活,推荐)
add_header Content-Security-Policy "frame-ancestors 'none'" always;
# 'none': 不允许嵌入
# 'self': 只允许同源
# https://trusted.com: 指定允许的源

安全HTTP头汇总

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Nginx安全头配置
server {
# 防止XSS
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;

# 强制HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# 限制Referrer信息泄露
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安全防御的核心原则:

  1. 永远不要信任用户输入:所有外部数据都必须验证和清理
  2. 使用参数化查询:这是防止SQL注入的最有效手段
  3. 输出编码要上下文感知:HTML、JS、URL、CSS各有不同的编码方式
  4. CSP是XSS的强力防线:限制脚本来源,防止内联脚本执行
  5. SameSite Cookie + CSRF Token:双重防御CSRF攻击
  6. SSRF防御需要验证目标IP:DNS解析后检查是否为内网地址
  7. 纵深防御:安全头、WAF、日志监控层层保护

安全不是一次性工作,需要持续的代码审计、依赖更新和渗透测试。

作者 · authorzt
发布 · date2025-06-29
篇幅 · length3.2k 字 · 8 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论