前端安全盲区深度解析:点击劫持、SQL注入与文件上传攻防实战
面试安全盲区清单:点击劫持(iframe 嵌套)、SQL 注入、文件上传漏洞与防御,把常被忽略的前端攻击讲透。 掌握后能补全安全知识体系,应对「还有哪些前端安全风险」的追问。
一句话概括
XSS 和 CSRF 人人喊打,但点击劫持、SQL 注入和文件上传漏洞才是那些”你以为后端会处理”但实际上可能全线崩溃的安全盲区——它们分别利用了用户意图欺骗、数据链路信任和输入假设上的认知漏洞。
核心知识点
1. 点击劫持:你看到的按钮,点到的却不是它
攻击者在自己的页面里用一个透明的 iframe 套住你的页面,用户以为在点击”领取红包”,实际点击的是被覆盖在下面的”确认转账”按钮。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 攻击者页面的核心代码 -->
<style>
.bait-btn { /* 用户看到的"点击抽奖"按钮 */ }
iframe {
position: absolute;
top: 200px; left: 150px;
width: 200px; height: 60px;
opacity: 0; /* 完全透明 */
z-index: 10; /* 浮在诱饵按钮上面 */
}
</style>
<button class="bait-btn">🎉 点击领取红包</button>
<iframe src="https://bank.com/transfer?amount=50000"></iframe>
<!-- 用户手指/鼠标点下去,实际命中的是透明 iframe 里的"确认转账" -->
一行代码防御(加上就安全 90%):
1
2
3
# 后端响应头加上这个
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
CSP 的 frame-ancestors 比 X-Frame-Options 更灵活(支持多域名),两个都设置覆盖所有浏览器。如果你有嵌入场景(如第三方 widget),改为 frame-ancestors 'self'。
2. SQL 注入:ORM 时代依然存在的暗箭
1
2
3
4
5
6
7
8
// ❌ 危险的字符串拼接(即使用了 ORM 也可能写出这种代码)
const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;
// 攻击者传 ?name=' OR '1'='1' --
// → SELECT * FROM users WHERE name = '' OR '1'='1' --' ← 返回所有用户!
// ✅ 参数化查询:让数据库分清"代码"和"数据"
const [rows] = await db.query('SELECT * FROM users WHERE name = ?', [req.query.name]);
// 攻击者输入 ' OR '1'='1' 会被当作字面字符串,不会被执行
ORM 也没覆盖的盲区:
1
2
3
4
5
6
7
// ❌ 动态排序字段不能被参数化
await prisma.$queryRawUnsafe(`SELECT * FROM posts ORDER BY ${req.query.sort}`);
// ✅ 必须用白名单
const ALLOWED = ['createdAt', 'title', 'views'];
const sort = ALLOWED.includes(req.query.sort) ? req.query.sort : 'createdAt';
await db.query(`SELECT * FROM posts ORDER BY ${sort} LIMIT ?`, [20]);
3. 文件上传:只检查扩展名等于没检查
攻击者上传 WebShell 的经典绕过方式:
1
2
3
4
5
6
7
POST /upload HTTP/1.1
Content-Type: multipart/form-data
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
Content-Type: image/jpeg ← 伪造的 MIME 类型!
<?php system($_GET['cmd']); ?> ← 实际上是 PHP 代码
多层验证防线:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { fileTypeFromFile } from 'file-type';
import sharp from 'sharp';
async function safeUpload(filePath) {
// 第1层:扩展名白名单
const ext = path.extname(filePath).toLowerCase();
if (!['.jpg', '.png', '.webp'].includes(ext)) throw new Error('类型不允许');
// 第2层:魔数检查(读文件开头的真实字节,不是信 Content-Type 头)
const type = await fileTypeFromFile(filePath);
if (!type?.mime.startsWith('image/')) throw new Error('非真实图片');
// 第3层:图片重编码(最安全)——解码再编码,剥离所有非图像数据
const safePath = filePath.replace(/\.[^.]+$/, '_safe.webp');
await sharp(filePath).resize({ width: 2000, withoutEnlargement: true }).webp().toFile(safePath);
return safePath;
}
// 重编码后即使原文件里藏了 PHP 代码,也会被彻底剥离
4. 三道防线对比
| 防线 | 防什么 | 能被绕过吗 |
|---|---|---|
| 扩展名检查 | 非白名单后缀 | ✅ 极易绕过(改名即可) |
| Content-Type 检查 | MIME 撒谎 | ✅ 易绕过(请求头可伪造) |
| 魔数检查 | 文件冒充 | ⚠️ 能防大部分,但 polyglot 文件可能绕过 |
| 重编码 | 一切非图像数据 | ❌ 几乎无法绕过 |
| 存储到 Web 根目录外 | 直接执行上传的脚本 | ❌ 物理隔离 |
5. 安全响应头速查(面试最爱问的一套)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 防点击劫持
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
# 防 XSS(禁止内联脚本,除非有 nonce)
Content-Security-Policy: script-src 'self'
# 防 MIME 类型嗅探攻击
X-Content-Type-Options: nosniff
# 强制 HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains
# 控制 referrer 信息泄露
Referrer-Policy: strict-origin-when-cross-origin
其实你每天都在用
- 点击劫持防御:打开支付宝/微信支付的确认页面,你没办法把它嵌在另一个网站里——就是因为他们设置了
X-Frame-Options: DENY - 参数化查询:你在淘宝搜”手机壳”,输入
' OR '1'='1不会泄露所有商品——因为后端用的是参数化查询而不是字符串拼接 - 图片重编码:上传微信头像时,即使你上传了一个 SVG 文件,微信也会把它转成 JPG——这就是在剥离可能的恶意脚本
- 文件随机命名:你上传的文件在服务器上不叫
shell.php而叫a3f8c2d1-e456-7890.jpg——避免了攻击者通过文件名猜测访问路径
常见误解(FAQ)
❌ 误区:点击劫持是后端的问题,前端管不着
后端能设置响应头,前端也可以用 JS 做防御降级:if (window.top !== window.self) window.top.location = window.self.location;——但如果攻击者在 iframe 上加了 sandbox 属性(不含 allow-top-navigation),JS 跳转会被拦截。响应头是主力,JS 是后备。
❌ 误区:用了 ORM/Prisma 就不会有 SQL 注入
ORM 只保护了标准 CRUD。一旦你用了 $queryRawUnsafe、$executeRawUnsafe,或者动态拼接表名/列名/排序字段,注入风险就回来了。任何涉及用户输入的字符串拼接进 SQL 的位置都需要白名单校验。
❌ 误区:前端文件类型校验(accept=”image/*“)就够了
前端的 accept 属性只是提示浏览器文件选择器,用户完全可以手动选择”所有文件”然后上传一个 .php。前端校验 100% 可绕过,后端才是真正的防线。
❌ 误区:SVG 是图片格式,上传 SVG 不会有安全问题
SVG 本质是 XML,里面可以嵌入 <script> 标签。如果 SVG 被直接引用(<img src="evil.svg">),脚本不执行;但如果被以 text/html 方式打开或内联到页面 DOM 中,脚本就会执行。最安全的做法是所有上传图片都转成位图格式(WebP/PNG)。
一句话总结
安全不是某一个角色的事——前端是第一道体验层过滤,后端是真正的防线,而响应头是最低成本却最容易被忽略的保险丝。