1999년식 비밀번호 저장을 그만: Node.js + MySQL 현실 점검
Source: Dev.to

당신이 만들고 있는 공포
당신은 방금 첫 번째 Node.js 인증 시스템을 만들었습니다. 똑똑해진 기분이죠. 그리고 이렇게 코드를 작성했습니다:
app.post('/register', (req, res) => {
const { username, password } = req.body;
db.query('INSERT INTO users (username, password) VALUES (?, ?)',
[username, password]); // 🚨 DISASTER
});
축하합니다! 데이터베이스가 유출될 때(언제가 아니라, 반드시), 모든 비밀번호가 코스트코의 무료 샘플처럼 남게 됩니다. 사람들은 비밀번호를 어디서든 재사용합니다. 그 "fluffy2023"? 해커에게 Sarah의 이메일, 은행, 인스타그램 접근 권한을 줬습니다.
솔루션: BCrypt는 모든 것을 저장합니다
BCrypt는 비밀번호를 알아볼 수 없을 정도로 섞어버리며, “되돌리기” 버튼이 없습니다. 해커들은 $2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW와 같은 난독화된 문자열을 보게 됩니다.
const bcrypt = require('bcrypt');
app.post('/register', async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
db.query('INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword]);
});
10은 salt rounds—얼마나 많은 섞는 사이클을 수행할지 나타냅니다. 2025년 기준으로 10은 좋은 기본값입니다.
실수 #2: 잘못된 비교
비밀번호를 해시했나요? 이 코드로 망치지 마세요:
// WRONG - Never works
if (results[0].password === password) {
res.send('Logged in!');
}
===를 사용해 해시된 값과 평문을 비교할 수 없습니다. 해결 방법:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
db.query('SELECT * FROM users WHERE username = ?', [username],
async (err, results) => {
const match = await bcrypt.compare(password, results[0].password);
if (match) {
res.send('Welcome!');
} else {
res.send('Try again');
}
});
});
bcrypt.compare()는 인증을 안전하게 처리합니다.
Source: …
실수 #3: 검증 없음
어떤 비밀번호든 받아들이는 것은 자살 행위입니다. "1"? 빈 문자열? 물론!
function isPasswordValid(password) {
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
return regex.test(password);
}
app.post('/register', async (req, res) => {
if (!isPasswordValid(req.body.password)) {
return res.status(400).send('Need 8+ chars, letters, numbers & symbols');
}
// continue with hashing...
});
실수 #4: SQL 인젝션
템플릿 문자열로 쿼리를 만들면 재앙을 초래합니다:
// NEVER
const query = `SELECT * FROM users WHERE username = '${username}'`;
admin'--와 같은 입력은 인증을 우회할 수 있습니다. 항상 파라미터화된 쿼리를 사용하세요:
db.query('SELECT * FROM users WHERE username = ?', [username]);
? 플레이스홀더는 입력을 코드가 아니라 데이터로 취급합니다.
완전 보안 예제
const express = require('express');
const bcrypt = require('bcrypt');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json());
async function initDb() {
return mysql.createPool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'your_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
}
const db = await initDb();
function isPasswordValid(password) {
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
return regex.test(password);
}
app.post('/register', async (req, res) => {
const { username, password } = req.body;
if (!isPasswordValid(password)) {
return res.status(400).json({ error: 'Weak password!' });
}
const hashedPassword = await bcrypt.hash(password, 10);
await db.query('INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword]);
res.json({ message: 'Account created!' });
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const [results] = await db.query('SELECT * FROM users WHERE username = ?',
[username]);
if (!results.length) {
return res.status(401).send('Invalid credentials');
}
const match = await bcrypt.compare(password, results[0].password);
if (match) {
res.json({ message: 'Login successful!' });
} else {
res.status(401).send('Invalid credentials');
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
깨우는 알림
2025년입니다. AI가 코드를 작성하고 자동차가 스스로 운전합니다. MySpace 시절의 비밀번호 저장 방식은 변명거리가 없습니다.
- BCrypt(또는 Argon2)로 비밀번호를 해시하세요.
- 입력 강도를 검증하세요.
- SQL 인젝션을 방지하기 위해 파라미터화된 쿼리를 사용하세요.
- 평문 비밀번호를 절대 저장하지 마세요.
사용자는 여러분을 신뢰합니다—내일의 보안 사고 헤드라인이 되지 않도록 하세요. 인터넷이 여러분을 찾기 전에 비밀번호를 안전하게 보호하세요.