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

발행: (2026년 2월 7일 오후 04:10 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

1999년처럼 비밀번호를 저장하지 마세요: Node.js + MySQL 현실 점검

당신이 만들고 있는 공포

당신은 방금 첫 번째 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]);
});

10salt 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 인젝션을 방지하기 위해 파라미터화된 쿼리를 사용하세요.
  • 평문 비밀번호를 절대 저장하지 마세요.

사용자는 여러분을 신뢰합니다—내일의 보안 사고 헤드라인이 되지 않도록 하세요. 인터넷이 여러분을 찾기 전에 비밀번호를 안전하게 보호하세요.

0 조회
Back to Blog

관련 글

더 보기 »

UX/UI 타이포그래피

Typography란 무엇을 의미할까요? - 어떤 font를 사용할지 - 어느 위치에서 얼마나 크게 할지 - 얼마나 굵게 할지 - 행 간격 - ...

이번 주 상위 7개 추천 DEV 게시물

이번 주 Top 7에 오신 것을 환영합니다. DEV 편집팀이 지난 주에 가장 좋아한 게시물을 직접 선정했습니다. 선정된 모든 저자분들께 축하드립니다.