SHIN
Node.js 실전 팁 20선
20편보안 취약점은 배포 후보다 개발 단계에서 막는 것이 훨씬 비용이 낮습니다.
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(128),
name: z.string().min(1).max(100).trim(),
});
app.post('/users', async (req, res) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
// result.data만 사용
});// ❌ 위험: 문자열 연결
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ Prisma/파라미터화 쿼리
const user = await prisma.user.findUnique({ where: { email } });
// Raw 쿼리가 필요할 때
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;npm install helmetimport helmet from 'helmet';
app.use(helmet()); // CSP, HSTS, X-Frame-Options 등 자동 설정
// 커스텀 CSP
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-{nonce}'"],
},
},
}));import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // IP당 최대 요청
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// 인증 엔드포인트는 더 엄격하게
const authLimiter = rateLimit({ windowMs: 60_000, max: 5 });
app.use('/api/auth/', authLimiter);import jwt from 'jsonwebtoken';
import { env } from './config/env.js';
// ❌ 약한 비밀 키, 만료 없음
jwt.sign({ userId }, 'secret');
// ✅ 강한 키, 짧은 만료
const token = jwt.sign(
{ userId, iat: Math.floor(Date.now() / 1000) },
env.JWT_SECRET, // 최소 32바이트 랜덤
{ expiresIn: '15m', algorithm: 'HS256' }
);import cors from 'cors';
app.use(cors({
origin: ['https://myapp.com', 'https://www.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}));
// ❌ origin: '*' with credentials: true 조합 불가npm audit # 취약점 스캔
npm audit fix # 자동 수정
npx snyk test # Snyk으로 심층 스캔
# CI에서 자동 체크
npm audit --audit-level=high// ❌ 스택 트레이스를 클라이언트에 노출
res.status(500).json({ error: err.stack });
// ✅ 프로덕션에서는 일반 메시지만
app.use((err, req, res, next) => {
const isProd = process.env.NODE_ENV === 'production';
res.status(err.statusCode || 500).json({
error: isProd ? '서버 오류가 발생했습니다' : err.message,
});
});import multer from 'multer';
const upload = multer({
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB 제한
fileFilter: (req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
cb(null, allowed.includes(file.mimetype));
},
});// 환경 변수 절대 응답에 포함 금지
app.get('/debug', (req, res) => {
// ❌ res.json(process.env)
res.json({ nodeVersion: process.version }); // 안전한 정보만
});