文章

前端安全盲区深度解析:点击劫持、SQL注入与文件上传攻防实战

面试安全盲区清单:点击劫持(iframe 嵌套)、SQL 注入、文件上传漏洞与防御,把常被忽略的前端攻击讲透。 掌握后能补全安全知识体系,应对「还有哪些前端安全风险」的追问。

前端安全盲区深度解析:点击劫持、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)。

一句话总结

安全不是某一个角色的事——前端是第一道体验层过滤,后端是真正的防线,而响应头是最低成本却最容易被忽略的保险丝。

本文由作者按照 CC BY 4.0 进行授权