关于 security-express
security-express 是面向 Express.js 应用的安全审计技能,整理中间件、身份验证和常见漏洞的检查模式。适用于审查和加固 Express.js 项目的开发者。
主要功能
- 01检查 Helmet 安全响应头
- 02审查 CORS 与请求体大小限制
- 03核查路由身份验证和中间件顺序
- 04识别注入、路径遍历与错误信息泄露
- 05检查会话安全和登录限速
适用场景
- 01审计 Express.js 项目的安全配置
- 02检查管理路由是否缺少身份验证
- 03查找用户输入处理中的常见漏洞
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ igorwarzocha/ opencode-workflows --skill security-express安装后在 Claude Code 中输入/security-express确认已加载。
附带文件scripts/scan.sh
详细介绍
针对 Express.js 应用的安全审计模式,涵盖必要的安全中间件、CORS 配置、身份验证模式和常见漏洞。
必要的安全中间件
Helmet.js(安全标头)
// ❌ Missing security headers
const app = express();
// ✓ Use Helmet
const helmet = require('helmet');
app.use(helmet());
检查是否已安装并使用 Helmet。 它会设置:
- Content-Security-Policy
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Strict-Transport-Security
- 等等……
禁用 X-Powered-By
// ❌ Default (header reveals framework)
const app = express();
// ✓ Disable fingerprinting
app.disable('x-powered-by');
// or: app.set('x-powered-by', false);
CORS 配置
// ❌ CRITICAL: Allow all origins
app.use(cors());
app.use(cors({ origin: '*' }));
// ❌ HIGH: Reflect origin with credentials
app.use(cors({
origin: true, // Reflects any origin!
credentials: true
}));
// ✓ Explicit allowlist
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
}));
// ✓ Function for dynamic validation
app.use(cors({
origin: (origin, callback) => {
const allowed = ['https://app.example.com'];
if (!origin || allowed.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));
请求体解析器限制
// ❌ No limit (DoS risk)
app.use(express.json());
// ✓ Set reasonable limits
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: true, limit: '100kb' }));
身份验证中间件模式
路由缺少身份验证
// ❌ No auth on admin routes
app.get('/api/admin/users', async (req, res) => {
res.json(await User.find());
});
// ✓ Auth middleware applied
app.get('/api/admin/users', requireAuth, requireAdmin, async (req, res) => {
res.json(await User.find());
});
中间件顺序很重要
// ❌ Wrong order - static files before auth
app.use(express.static('uploads')); // Exposed!
app.use(requireAuth);
// ✓ Auth before protected static files
app.use('/public', express.static('public')); // Intentionally public
app.use(requireAuth);
app.use('/uploads', express.static('uploads')); // Now protected
路由器级别的身份验证漏洞
// Check: Is auth applied to all routes in admin router?
const adminRouter = express.Router();
adminRouter.use(requireAuth); // Applied to all routes below
adminRouter.get('/users', getUsers);
adminRouter.delete('/users/:id', deleteUser);
// ❌ Watch for routes defined BEFORE the middleware
const apiRouter = express.Router();
apiRouter.get('/health', getHealth); // No auth (intentional?)
apiRouter.use(requireAuth);
apiRouter.get('/users', getUsers); // Has auth
常见漏洞
SQL/NoSQL 注入
// ❌ String interpolation
const user = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// ✓ Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
// ❌ MongoDB injection
const user = await User.findOne({ email: req.body.email }); // If email is { $gt: "" }
// ✓ Validate input type
if (typeof req.body.email !== 'string') return res.status(400).json({ error: 'Invalid email' });
路径遍历
// ❌ User-controlled path
app.get('/files/:filename', (req, res) => {
res.sendFile(`./uploads/${req.params.filename}`); // ../../etc/passwd
});
// ✓ Validate and normalize
const path = require('path');
app.get('/files/:filename', (req, res) => {
const filename = path.basename(req.params.filename);
const filepath = path.join(__dirname, 'uploads', filename);
if (!filepath.startsWith(path.join(__dirname, 'uploads'))) {
return res.status(400).json({ error: 'Invalid path' });
}
res.sendFile(filepath);
});
错误处理
// ❌ Stack traces in production
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack }); // Leaks internals
});
// ✓ Safe error handler
app.use((err, req, res, next) => {
console.error(err); // Log for debugging
res.status(500).json({ error: 'Internal server error' });
});
会话安全
// ❌ Insecure session config
app.use(session({
secret: 'keyboard cat', // Hardcoded!
cookie: { secure: false }, // No HTTPS requirement
}));
// ✓ Secure config
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JS access
sameSite: 'strict', // CSRF protection
maxAge: 1000 * 60 * 60 * 24, // 24 hours
},
}));
速率限制
// Check for rate limiting on auth routes
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts',
});
app.post('/api/login', authLimiter, loginHandler);
app.post('/api/register', authLimiter, registerHandler);
app.post('/api/forgot-password', authLimiter, forgotPasswordHandler);
快速审计命令
# Check if Helmet is used
rg -n 'helmet\\(' . -g "*.js" -g "*.ts"
# Check if x-powered-by is disabled
rg -n "x-powered-by" . -g "*.js" -g "*.ts"
# Check for helmet
rg "helmet" package.json
rg "require\\(['\"]helmet" .
rg "from ['\"]helmet" .
# Find CORS config
rg "cors\\(" . -g "*.js" -g "*.ts" -A 5
# Find routes without auth middleware
rg "app\\.(get|post|put|delete|patch)\\(" . -A 1 | grep -v "require.*[Aa]uth"
# Find string interpolation in queries
rg "(query|find|findOne|exec).*\\`" . -g "*.js" -g "*.ts"
# Check session config
rg "session\\(" . -A 10
安全加固检查清单
- 已安装并使用 Helmet.js
- CORS 限制为特定来源
- 请求体解析器设置了大小限制
- 所有受保护的路由均使用身份验证中间件
- 身份验证端点已设置速率限制
- 会话 Cookie:secure、httpOnly、sameSite
- 没有硬编码的密钥
- 错误处理程序不会泄露堆栈跟踪
- 对所有用户输入进行验证
- 使用参数化查询(不拼接字符串)
评论
支持 Markdown