LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

Web 三大攻击防御:XSS、CSRF、SQL 注入到底该怎么挡

admin
2026年7月18日 23:22 本文热度 48

每隔几个月就会刷到一条新闻:「某公司用户数据泄露」「某电商被薅羊毛」「某后台被拖库」。十有八九绕不开 Web 安全的老三样——XSS、CSRF、SQL 注入

这三样不是新东西,OWASP Top 10 里挂了十几年了。但每年都有新人踩、老人也踩。今天这篇就把它们到底怎么打、怎么防讲清楚。每种攻击都给一个教学版的最小复现 demo(请勿用于真实环境),再给生产可用的防御方案。


一、XSS:跨站脚本攻击

核心定义:攻击者把恶意 JS 注入你的页面,在受害者浏览器里跑起来,能偷 cookie、改 DOM、调你的接口。

三种形态

类型
攻击点
例子
反射型
URL 参数直接渲染到页面
https://app.com/search?q=<script>...</script>
存储型
攻击 payload 存进数据库,所有访问者都中招
评论里写 <script>fetch('/api/...')</script>
DOM 型
完全前端,URL hash / innerHTML 操作
location.hash
 直接塞 innerHTML

存储型最危险(影响所有访问者),DOM 型最隐蔽(流量不经过后端,WAF 抓不到)。

最小复现 demo(教学版,勿用于真实环境)

   html

<div id="content"></div>
<script>
  const userInput = new URLSearchParams(location.search).get('msg');
  // 反射型 XSS:访问 ?msg=<img src=x onerror=alert(1)>
  document.getElementById('content').innerHTML = userInput;
</script>

注意:直接 <script>...</script> 在 innerHTML 里不会执行(浏览器规定),但 <img onerror> 这类带事件的 HTML 会触发。所以用 innerHTML 仍然非常危险。

防御核心:输出转义 + CSP 兜底

1. 输出转义按位置区分(这是新人最容易搞错的)

输出位置
转义规则
HTML body(<div>x</div>
< > & " '
 转 HTML 实体
HTML 属性(<a title="x">
同上 + 必须加引号包裹
JS 字符串(var x = "...";
\u0000
 形式转义所有非字母数字
URL 参数(<a href="?q=x">
encodeURIComponent
CSS(<style>...</style>
仅允许白名单

前端框架默认帮你转:React 用 {userInput} 是安全的;Vue 用 {{ userInput }} 是安全的。

翻车的总是这几个

   jsx
// ❌ React 的 dangerouslySetInnerHTML(名字已经在警告你了)
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// ❌ Vue 的 v-html
<div v-html="userInput"></div>

// ❌ 任何直接的 innerHTML / outerHTML / document.write
el.innerHTML = userInput;

实在要渲染富文本 → 用专门的 sanitizer:

   js
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput, {
  ALLOWED_TAGS: ['b''i''em''strong''a''p''br'],
  ALLOWED_ATTR: ['href'],
});
el.innerHTML = clean;

2. URL 参数防御

特别注意 href / src 别让用户输入 javascript: 开头:

   js
function safeUrl(url) {
  const trimmed = String(url).trim().toLowerCase();
  if (trimmed.startsWith('javascript:') || trimmed.startsWith('data:')) {
    return '#';
  }
  return url;
}


二、CSP:内容安全策略(XSS 兜底)

CSP 是浏览器层的 XSS 兜底——即使代码里有漏洞,浏览器也按 CSP 规则拒绝执行可疑脚本

通过 HTTP 响应头下发:

   
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; report-uri /csp-report

关键指令

指令
作用
default-src
兜底,没明确指定的资源类型走这个
script-src
JS 来源(最重要)
style-src
CSS 来源
img-src
图片来源
connect-src
XHR / fetch / WebSocket 能连哪些
frame-ancestors
谁能 iframe 嵌入我(替代 X-Frame-Options)
report-uri
 / report-to
违反 CSP 时上报到哪

生产可用样板

新项目推荐:用 nonce + strict-dynamic(最强 XSS 防御)

   
Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-aBcDeF12345' 'strict-dynamic';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  object-src 'none';
  base-uri 'self';
  upgrade-insecure-requests;
  report-uri /api/csp-report;

服务端每个请求生成随机 nonce,模板里 <script nonce="aBcDeF12345"> 才能执行:

   js
// Express 示例
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  res.setHeader('Content-Security-Policy',
    `script-src 'nonce-${res.locals.nonce}' 'strict-dynamic'; ...`);
  next();
});
   html
<script nonce="<%= nonce %>">/* 你的内联脚本 */</script>

灰度上线:Report-Only

直接上线 CSP 容易把页面打挂(第三方脚本被拦),先用 Report-Only 模式跑一周:

   
Content-Security-Policy-Report-Only: ...一样的策略...

这个 header 只上报、不拦截。看违规日志发现哪些资源被漏写了,调完策略再切正式 header。

避免错误

  • ❌ CSP 里写 script-src 'unsafe-inline' 等于没写
  • ❌ 抄网上 CSP 样板要看清楚版本(strict-dynamic 是 CSP3 才有)
  • ❌ 默认的 CSP 主要拦"加载/执行外部脚本"那条线,对 DOM 型 XSS(如把脏数据塞进 innerHTML)防御能力有限——要彻底治 DOM XSS,得叠加 require-trusted-types-for 'script'(CSP3 引入,浏览器兼容性还在追赶),同时输出转义这步永远不能省

三、CSRF:跨站请求伪造

核心定义:你登录了 bank.com,浏览器里有 cookie;攻击者诱导你访问 evil.com,evil.com 上一个 <form action="https://bank.com/transfer" method="post"> 自动提交,浏览器自动带上 bank.com 的 cookie,钱就转走了。

CSRF 的关键在于「浏览器自动带 cookie」这个特性。

防御四件套

1. SameSite Cookie(现代浏览器自带的防线)

   
Set-Cookie: sessionid=abc; SameSite=Strict; Secure; HttpOnly
取值
行为
Strict
跨站请求完全不带这个 cookie
Lax
(Chrome 默认)
顶级导航的 GET 请求会带,其他跨站请求不带
None
跨站正常带(必须配 Secure,否则被浏览器拒收)

Chrome 80+ 默认就是 Lax,等于默认就帮你挡了大部分 CSRF

但有几个场景因此挂了:

  • iframe 嵌入第三方系统、跨站 SSO 回跳:可能需要 SameSite=None; Secure
  • POST 跨站表单(合法第三方支付回调):同上

2. CSRF Token(最传统也最有效)

服务端给每个会话生成一个不可预测的 token,放在表单或自定义 header 里,每次写操作都校验

   html
<form method="post" action="/transfer">
  <input type="hidden" name="_csrf" value="aBcDeF...">
  ...
</form>
   js
// 后端校验
if (req.body._csrf !== req.session.csrfToken) {
  return res.status(403).send('CSRF token mismatch');
}

跨站攻击者没法读到你站点的 token(同源策略),所以伪造不出合法请求。

Express 用 csurf(已停止维护,新项目推荐 csrf-csrf 或框架自带方案);Spring Security 默认开启 CSRF Token。

3. 双重提交 Cookie(前后端分离友好)

服务端给客户端发一个 cookie(如 XSRF-TOKEN),前端读出来塞到自定义 header(如 X-XSRF-TOKEN)发请求。

服务端校验「cookie 里的值 == header 里的值」。

因为攻击者:

  • 能让浏览器自动带上 cookie(伪造请求)
  • 但读不到你域名下的 cookie
    (同源限制),自然没法把值放 header

Axios / Angular 默认支持这个模式。

4. Origin / Referer 校验

每个跨域请求浏览器自动带 Origin header(或 Referer),后端简单校验:

   js
const allowedOrigins = ['https://app.example.com'];
if (req.method !== 'GET' && !allowedOrigins.includes(req.headers.origin)) {
  return res.status(403).send('Forbidden');
}

作为兜底防线用,不能当主防御(Origin 在某些场景可能没有)。

教学版 demo

evil.com 上的一行 HTML 就能搞事(仅教学):

   html

<form action="https://bank.com/transfer" method="post">
  <input name="to" value="attacker">
  <input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>

如果 bank.com 没做任何 CSRF 防护,你登录状态下访问 evil.com 钱就飞了。

真实业务里最低标准:写操作(POST / PUT / DELETE)必须校验 CSRF Token 或 SameSite=Lax 以上。


四、SQL 注入:预编译为什么能挡

核心定义:攻击者把 SQL 语法塞进用户输入,让你的拼接 SQL 变成攻击者想要的。

教学版 demo

错误版(拼接):

   js
// 用户输入 username = "admin' OR '1'='1"
const sql = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
// 实际执行:
// SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '...'
// 攻击者直接登录成功

正确版(参数化查询):

   js
// 用 ? 占位符
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(sql, [username, password]);
// 即使 username 是 "admin' OR '1'='1",也只是当成一个普通字符串值

预编译为什么能挡?

数据库收到带占位符的 SQL 时先编译执行计划,再把参数作为「纯数据」绑进去——SQL 结构已经定型,参数里写什么 SQL 语法都不再生效。

这是结构性防御,跟字符串里有没有引号无关,比"过滤危险字符"靠谱 10 倍。

ORM 不等于安全

很多人觉得「我用 ORM 了不用担心」。错。ORM 的 raw query / 字符串拼接的字段名都会翻车

   js
// ❌ Sequelize - 拼接 ORDER BY 字段名(占位符无效)
User.findAll({ order: [[req.query.sortField'ASC']] });
// 攻击者传 sortField = "(SELECT ...)" 就出事

// ❌ MyBatis 的 $ 拼接
SELECT * FROM users WHERE id = ${id}  // 字符串拼接,危险
SELECT * FROM users WHERE id = #{id}  // 预编译,安全

// ❌ MongoDB 的 $where 接受字符串
db.users.find({ $where`this.name == '${userName}'` })  // NoSQL 注入

永远遵循

  • 字符串值用占位符 / 参数化
  • 字段名、表名做白名单(动态字段必须是预定义列表里的值)
  • raw query 当作最后手段,能用 ORM 标准 API 别用 raw

五、文件上传攻击

经典套路:上传一个 shell.php,访问 https://app.com/uploads/shell.php 直接拿 webshell。

四道防御

1. 扩展名白名单(不是黑名单)

   js
const allowed = ['.jpg''.png''.gif''.pdf'];
const ext = path.extname(file.originalname).toLowerCase();
if (!allowed.includes(ext)) {
  return res.status(400).send('Invalid file type');
}

避免:黑名单容易漏(.phtml.php5.htaccess),永远用白名单。

2. 文件头 magic number 校验

   js
import FileType from 'file-type';
const type = await FileType.fromBuffer(fileBuffer);
if (!['image/jpeg''image/png''image/gif'].includes(type?.mime)) {
  return res.status(400).send('Invalid file content');
}

防御「把 shell.php 改名 shell.jpg」的绕过。

3. 重命名 + 存储隔离

   js
const safeName = `${crypto.randomUUID()}${ext}`;   // 随机命名
const uploadPath = `/data/uploads/${safeName}`;     // 存到独立目录

最关键的隔离:上传目录单独域名 + 禁止执行

   nginx
# nginx 配置:上传域名禁止跑任何动态脚本
server {
  server_name uploads.example.com;
  root /data/uploads;
  
  location ~ \.(php|jsp|asp|aspx|cgi)$ {
    deny all;     # 直接拒绝
  }
}

或者干脆把文件存对象存储(OSS / S3),返回的就是文件 URL,没办法执行。

4. 大小限制 + 病毒扫描(敏感场景)

   js
multer({
  limits: { fileSize5 * 1024 * 1024 },   // 5MB
  fileFilter: ...
})


六、路径穿越与 SSRF(带过)

路径穿越:用户传文件名 ../../../etc/passwd,服务端拼接路径读取。

   js
// ❌ 危险
res.sendFile(path.join('/data/files', req.params.filename));

// ✅ 校验最终路径仍在白名单目录内
const baseDir = path.resolve('/data/files');
const target = path.resolve(baseDir, req.params.filename);
if (!target.startsWith(baseDir + path.sep)) {
  return res.status(400).send('Bad path');
}
res.sendFile(target);

SSRF:用户输入 URL,服务端代为请求。攻击者传 http://169.254.169.254/latest/meta-data/(云厂商元数据服务)就能偷你的 IAM 凭证。

   js
// 防御:白名单域名 + 拦截内网 IP / 元数据地址
const blocked = ['127.0.0.1''localhost''169.254.169.254''10.''192.168.''172.16.'];

云上服务一定要把元数据访问加强(AWS 用 IMDSv2、阿里云配 ram trust 限制)。


七、安全测试工具

工具
干嘛的
sqlmap
SQL 注入自动化扫描,能从一个有漏洞的 URL 直接 dump 全库
OWASP ZAP
开源 Web 安全扫描器,能集成 CI
Burp Suite
业界标杆,社区版够用,专业版能做更深的 Fuzz
Semgrep / Bandit
源码层 SAST,可集成 CI 静态扫描安全反模式
DependencyTrack / Snyk
第三方依赖漏洞扫描

CI 里建议至少跑 Semgrep + Snyk,能拦住 80% 的低级漏洞。


八、三大攻击对比表

维度
XSS
CSRF
SQL 注入
攻击点
用户输入被渲染到页面
浏览器自动带 cookie
用户输入被拼接到 SQL
利用条件
输出未转义 / 没 CSP
写接口未防伪造
拼接 SQL / raw query
核心防御
转义 + DOMPurify + CSP
CSRF Token / SameSite
参数化查询
兜底防御
CSP report-only 灰度
Origin/Referer 校验
最小权限 DB 账号
典型场景
评论区 / 用户简介
转账 / 改密码
登录 / 搜索框


九、写在最后

Web 安全没有银弹,但有几条铁律记住基本不会出大事:

  1. 永远不信任用户输入
    (前端校验只是 UX,服务端校验才是安全)
  2. 服务端是最后一道防线
    (别指望前端能挡)
  3. 白名单优于黑名单
    (黑名单永远列不全)
  4. 结构性防御优于过滤
    (参数化查询 / 输出转义 vs 字符串黑名单)
  5. 纵深防御
    (CSP 是 XSS 兜底、SameSite 是 CSRF 兜底,多层叠加)

最后说一句:别用 ORM 就以为自己安全,别用 React 就以为没 XSS。框架默认安全,但 dangerouslySetInnerHTML 和 raw query 这种逃生舱用错了就裸奔。


阅读原文:点击这里


该文章在 2026/7/18 23:22:37 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-9  粤公网安备44030602007207号