Node.js 인터뷰 질문 58
질문: Node.js 애플리케이션의 주요 보안 취약점과 이를 방지하기 위한 모범 사례는 무엇인가요?
답변:
Node.js 애플리케이션은 웹 서비스나 API를 구축할 때 널리 사용되지만, 보안에 주의를 기울이지 않으면 다양한 취약점에 노출될 수 있습니다. 주요 보안 취약점과 이를 방지하기 위한 모범 사례에 대해 알아보겠습니다.
1. 주요 보안 취약점
1.1 인젝션 공격 (Injection Attacks)
SQL 인젝션, NoSQL 인젝션, 명령어 인젝션 등이 이에 해당합니다.
// SQL 인젝션에 취약한 코드
const query = `SELECT * FROM users WHERE username = '${req.body.username}'`;
db.query(query);
1.2 크로스 사이트 스크립팅 (XSS)
사용자 입력이 적절히 이스케이프되지 않고 클라이언트에게 반환될 때 발생합니다.
// XSS에 취약한 코드
app.get("/profile", (req, res) => {
res.send(`<div>Welcome, ${req.query.name}!</div>`);
});
1.3 취약한 의존성 (Vulnerable Dependencies)
오래되거나 보안 취약점이 있는 npm 패키지를 사용할 때 발생합니다.
1.4 부적절한 인증 및 권한 부여
사용자 인증과 권한 검사가 부적절하게 구현될 때 발생합니다.
1.5 민감한 데이터 노출
암호화 없이 민감한 정보를 저장하거나 전송할 때 발생합니다.
1.6 보안 설정 오류
기본 설정 그대로 사용하거나 운영 환경에서 디버그 모드를 활성화하는 등의 문제가 여기에 해당합니다.
2. 인젝션 공격 방지
2.1 SQL 인젝션 방지
매개변수화된 쿼리나 ORM을 사용하여 방지합니다:
// 매개변수화된 쿼리 사용 (MySQL)
const query = "SELECT * FROM users WHERE username = ?";
db.query(query, [req.body.username]);
// Sequelize ORM 사용
const user = await User.findOne({
where: { username: req.body.username },
});
2.2 NoSQL 인젝션 방지
데이터 타입 검증 및 변환을 통해 방지합니다:
// MongoDB 인젝션에 취약한 코드
const query = { username: req.body.username };
db.collection("users").find(query);
// 개선된 코드: 타입 검증 및 변환
const username = String(req.body.username);
const query = { username: username };
db.collection("users").find(query);
2.3 명령어 인젝션 방지
child_process.exec
대신 child_process.execFile
을 사용하거나 shell 옵션을 비활성화합니다:
// 취약한 코드
const { exec } = require("child_process");
exec(`ping ${userInput}`); // 인젝션 위험
// 개선된 코드
const { execFile } = require("child_process");
execFile("ping", [userInput]); // 더 안전함
3. 크로스 사이트 스크립팅(XSS) 방지
3.1 입력 유효성 검사 및 출력 이스케이프
const escapeHtml = require("escape-html");
app.get("/profile", (req, res) => {
// 사용자 입력 이스케이프
const name = escapeHtml(req.query.name || "");
res.send(`<div>Welcome, ${name}!</div>`);
});
3.2 Content Security Policy (CSP) 구현
const helmet = require("helmet");
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
// 다른 지시문 추가
},
})
);
3.3 HttpOnly 및 Secure 쿠키 사용
app.use(
session({
secret: "your-secret-key",
cookie: {
httpOnly: true, // JavaScript에서 쿠키 접근 방지
secure: true, // HTTPS에서만 쿠키 전송
sameSite: "strict", // CSRF 방지
},
})
);
4. 의존성 관리
4.1 정기적인 의존성 취약점 검사
# npm audit으로 취약점 검사
npm audit
# 자동 수정 시도
npm audit fix
# 의존성 업데이트
npm update
4.2 package-lock.json 또는 yarn.lock 사용
정확한 버전의 종속성을 보장하기 위해 lock 파일을 사용합니다.
4.3 Snyk 또는 npm audit과 같은 도구를 CI/CD 파이프라인에 통합
# GitHub Actions 워크플로우 예시
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 노드 설정
uses: actions/setup-node@v2
with:
node-version: "14"
- name: 의존성 설치
run: npm ci
- name: 보안 취약점 검사
run: npm audit --audit-level=high
5. 인증 및 권한 부여
5.1 강력한 패스워드 정책 구현
const passwordValidator = require("password-validator");
const schema = new passwordValidator();
schema
.is()
.min(8) // 최소 8자
.is()
.max(100) // 최대 100자
.has()
.uppercase() // 대문자 포함
.has()
.lowercase() // 소문자 포함
.has()
.digits(2) // 최소 2개의 숫자
.has()
.not()
.spaces() // 공백 금지
.has()
.symbols(1); // 최소 1개의 특수문자
// 사용 예
app.post("/register", (req, res) => {
if (!schema.validate(req.body.password)) {
return res
.status(400)
.json({ error: "보안 요구사항을 충족하지 않는 비밀번호" });
}
// 계속 진행...
});
5.2 비밀번호 해싱
const bcrypt = require("bcrypt");
// 비밀번호 해싱
async function hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
// 해시 검증
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// 사용 예
app.post("/register", async (req, res) => {
try {
const hashedPassword = await hashPassword(req.body.password);
// 해시된 비밀번호 저장
// ...
} catch (error) {
// 오류 처리
}
});
5.3 JWT 보안 강화
const jwt = require("jsonwebtoken");
// JWT 생성
function generateToken(user) {
return jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
expiresIn: "1h", // 짧은 만료 시간
algorithm: "HS256", // 알고리즘 명시
});
}
// JWT 검증
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
return null;
}
}
5.4 API 속도 제한 구현
const rateLimit = require("express-rate-limit");
// 로그인 요청 제한
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 5, // 15분 동안 IP당 최대 5번 요청
message: "너무 많은 로그인 시도, 나중에 다시 시도하세요.",
});
app.post("/login", loginLimiter, (req, res) => {
// 로그인 로직
});
// 전체 API 요청 제한
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1분
max: 100, // 1분 동안 IP당 최대 100번 요청
message: "너무 많은 요청, 나중에 다시 시도하세요.",
});
app.use("/api/", apiLimiter);
6. 민감한 데이터 보호
6.1 환경 변수 사용
// .env 파일에 시크릿 저장
// DB_PASSWORD=your-secure-password
// API_KEY=your-api-key
require("dotenv").config();
// 환경 변수 사용
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
};
6.2 데이터 암호화
const crypto = require("crypto");
// 대칭 암호화 함수
function encrypt(text) {
const algorithm = "aes-256-cbc";
const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
return { iv: iv.toString("hex"), encryptedData: encrypted };
}
// 복호화 함수
function decrypt(encryptedObj) {
const algorithm = "aes-256-cbc";
const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
const iv = Buffer.from(encryptedObj.iv, "hex");
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encryptedObj.encryptedData, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
6.3 HTTPS 사용
const https = require("https");
const fs = require("fs");
const express = require("express");
const app = express();
// HTTPS 설정
const options = {
key: fs.readFileSync("path/to/private-key.pem"),
cert: fs.readFileSync("path/to/certificate.pem"),
};
// HTTPS 서버 생성
https.createServer(options, app).listen(443, () => {
console.log("HTTPS 서버가 포트 443에서 실행 중입니다");
});
7. 보안 헤더 설정
7.1 Helmet 사용
const helmet = require("helmet");
const express = require("express");
const app = express();
// 기본 보안 헤더 설정
app.use(helmet());
// 커스텀 보안 헤더 설정
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "trusted-scripts.com"],
styleSrc: ["'self'", "trusted-styles.com"],
imgSrc: ["'self'", "data:", "trusted-images.com"],
connectSrc: ["'self'", "api.trusted-source.com"],
},
},
referrerPolicy: { policy: "same-origin" },
xFrameOptions: { action: "deny" },
})
);
8. 오류 처리 및 로깅
8.1 안전한 오류 처리
// 404 핸들러
app.use((req, res, next) => {
res.status(404).json({ error: "요청한 리소스를 찾을 수 없습니다" });
});
// 500 핸들러
app.use((err, req, res, next) => {
// 개발 환경에서만 자세한 오류 표시
const error =
process.env.NODE_ENV === "production"
? "서버 오류가 발생했습니다"
: err.message;
// 오류 로깅
console.error("서버 오류:", err);
res.status(500).json({ error });
});
8.2 보안 로깅
const winston = require("winston");
// 로거 구성
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
// 파일에 로그 저장
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" }),
],
});
// 프로덕션이 아닌 경우 콘솔에도 로그 출력
if (process.env.NODE_ENV !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
}
// 로그 사용 예
app.use((req, res, next) => {
// 개인 정보 필터링
const logData = {
method: req.method,
path: req.path,
ip: req.ip,
// 민감한 데이터 제외 (예: req.body.password)
};
logger.info("요청 수신", logData);
next();
});
9. 입력 유효성 검사
9.1 Joi 검증 라이브러리 사용
const Joi = require("joi");
// 사용자 생성 스키마
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{8,30}$")).required(),
age: Joi.number().integer().min(18).max(120),
});
// 미들웨어로 사용
app.post("/users", (req, res, next) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// 검증 통과 시 계속 진행
req.validatedData = value;
next();
});
9.2 Express-Validator 사용
const { body, validationResult } = require("express-validator");
app.post(
"/users",
// 유효성 검증 규칙
[
body("username").isAlphanumeric().isLength({ min: 3, max: 30 }),
body("email").isEmail().normalizeEmail(),
body("password")
.isLength({ min: 8 })
.matches(/^[a-zA-Z0-9]{8,30}$/),
body("age").isInt({ min: 18, max: 120 }),
],
// 검증 결과 처리
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 검증 통과 시 계속 진행
// ...
}
);
10. CSRF 공격 방지
10.1 CSRF 토큰 사용
const csrf = require("csurf");
const cookieParser = require("cookie-parser");
// CSRF 미들웨어 설정
app.use(cookieParser());
app.use(csrf({ cookie: true }));
// 폼에 CSRF 토큰 제공
app.get("/form", (req, res) => {
res.render("form", { csrfToken: req.csrfToken() });
});
// CSRF 토큰 검증
app.post("/submit", (req, res) => {
// csurf 미들웨어가 자동으로 토큰 검증
// 유효하지 않은 토큰은 403 오류 발생
res.send("양식 제출 성공");
});
// CSRF 오류 처리
app.use((err, req, res, next) => {
if (err.code === "EBADCSRFTOKEN") {
return res.status(403).json({ error: "CSRF 토큰이 유효하지 않습니다" });
}
next(err);
});
11. 보안 모니터링 및 감사
11.1 보안 로깅 및 모니터링
const express = require("express");
const morgan = require("morgan");
const winston = require("winston");
const app = express();
// HTTP 요청 로깅
app.use(
morgan("combined", {
stream: {
write: (message) => {
// Winston 로거로 로그 전달
logger.info(message.trim());
},
},
// 민감한 데이터 제외
skip: (req, res) =>
req.path === "/health" || req.path.startsWith("/static"),
})
);
// 인증 시도 로깅
app.post("/login", (req, res) => {
// 성공 여부에 따라 로깅
const success = // 인증 로직
logger.info("로그인 시도", {
username: req.body.username,
ip: req.ip,
userAgent: req.headers["user-agent"],
success,
});
// 로그인 로직 계속
});
11.2 보안 감사
// 관리자 작업 감사 로깅
function auditLog(userId, action, details) {
const auditEntry = {
userId,
action,
details,
timestamp: new Date().toISOString(),
ip: this.req.ip,
userAgent: this.req.headers["user-agent"],
};
// 감사 로그 저장
logger.info("관리자 작업", auditEntry);
db.collection("audit_logs").insertOne(auditEntry);
}
// 사용 예
app.delete("/users/:id", isAdmin, (req, res) => {
const userId = req.params.id;
// 사용자 삭제 로직
db.collection("users").deleteOne({ _id: userId });
// 감사 로그 기록
auditLog(req.user.id, "DELETE_USER", {
deletedUserId: userId,
});
res.status(200).json({ message: "사용자가 삭제되었습니다" });
});
12. 코드 보안 검사 도구
12.1 ESLint 보안 규칙 설정
// .eslintrc.js
module.exports = {
extends: ["eslint:recommended", "plugin:security/recommended"],
plugins: ["security"],
rules: {
"security/detect-eval-with-expression": "error",
"security/detect-non-literal-regexp": "error",
"security/detect-non-literal-require": "error",
"security/detect-possible-timing-attacks": "error",
},
};
12.2 SonarQube 또는 Snyk와 같은 도구를 통한 정적 코드 분석
# Snyk CLI 사용
npm install -g snyk
snyk test
# CI/CD에 통합
snyk test --severity-threshold=high
13. 컨테이너 및 배포 보안
13.1 Docker 보안 모범 사례
# 비루트 사용자로 실행
FROM node:14-slim
# 작업 디렉토리 생성
WORKDIR /app
# 의존성 설치
COPY package*.json ./
RUN npm ci --only=production
# 소스 코드 복사
COPY . .
# 비루트 사용자 생성 및 권한 설정
RUN addgroup --system appgroup && \
adduser --system --ingroup appgroup appuser && \
chown -R appuser:appgroup /app
# 비루트 사용자로 전환
USER appuser
# 포트 및 실행 명령
EXPOSE 3000
CMD ["node", "server.js"]
14. 서버 보안 구성
14.1 NODE_ENV 설정
// 프로덕션 환경에서 보안 강화
if (process.env.NODE_ENV === "production") {
// 디버그 출력 비활성화
app.set("env", "production");
app.disable("x-powered-by");
// strict 모드 활성화
("use strict");
// 오류 세부 정보 숨기기
app.use((err, req, res, next) => {
res.status(500).json({ error: "서버 오류" });
console.error(err);
});
}
14.2 클린 종료 처리
// 정상 종료 처리
function shutdown() {
console.log("애플리케이션을 정상 종료합니다...");
// 열린 연결 종료
server.close(() => {
console.log("HTTP 서버 종료됨");
// 데이터베이스 연결 종료
mongoose.connection.close(false, () => {
console.log("MongoDB 연결 종료됨");
process.exit(0);
});
});
// 강제 종료 타임아웃
setTimeout(() => {
console.error("정상 종료에 실패했습니다, 강제 종료합니다");
process.exit(1);
}, 10000);
}
// 종료 신호 처리
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
요약: Node.js 애플리케이션 보안 모범 사례
인젝션 공격 방지:
- 매개변수화된 쿼리 사용
- ORM 활용
- 입력 데이터 타입 검증
크로스 사이트 스크립팅(XSS) 방지:
- 입력 유효성 검사 및 출력 이스케이프
- Content Security Policy 구현
- HttpOnly 및 Secure 쿠키 사용
의존성 관리:
- 정기적인 의존성 취약점 검사 (npm audit)
- 최신 패키지 버전 유지
- CI/CD 파이프라인에 보안 검사 통합
인증 및 권한 부여:
- 강력한 비밀번호 정책
- 보안 해싱 알고리즘 (bcrypt)
- JWT 보안 강화
- API 속도 제한
민감한 데이터 보호:
- 환경 변수 사용
- 데이터 암호화
- HTTPS 사용
보안 헤더 설정:
- Helmet 사용
- 적절한 CORS 설정
오류 처리 및 로깅:
- 안전한 오류 처리
- 구조화된 로깅
- 민감한 정보 필터링
입력 유효성 검사:
- Joi, Express-Validator 등 검증 라이브러리 사용
- 모든 사용자 입력 검증
CSRF 공격 방지:
- CSRF 토큰 사용
- SameSite 쿠키 속성 설정
보안 모니터링 및 감사:
- 보안 로깅
- 중요 작업 감사 추적
- 모니터링 시스템 구축
Node.js 애플리케이션의 보안은 개발 초기 단계부터 지속적으로 고려되어야 합니다. 위의 모범 사례들을 적용하고 정기적인 보안 감사와 모니터링을 통해 애플리케이션의 보안을 강화할 수 있습니다. 또한 보안은 지속적인 과정이므로 최신 보안 위협과 대응 방법에 대해 항상 업데이트된 정보를 유지하는 것이 중요합니다.