July 16, 2025

๐Ÿ›ก๏ธ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ํด๋ผ์ด์–ธํŠธ ์Šคํ† ๋ฆฌ์ง€ ์˜ค๋‚จ์šฉ: XSS ๊ณต๊ฒฉ์˜ ๋จน์ž‡๊ฐ

JavaScript
๋ณด์•ˆ
์›นํ‘œ์ค€
UX
์—๋Ÿฌ์ฒ˜๋ฆฌ

Summary

๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋ฅผ localStorage๋‚˜ sessionStorage์— ์ง์ ‘ ์ €์žฅํ•˜๋Š” ๊ฒƒ์€ XSS ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•˜์—ฌ ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์œ„ํ—˜์„ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ ํ† ํฐ์€ HttpOnly ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋ฏผ๊ฐํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์— ์ €์žฅํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ตœ์†Œํ•œ์˜ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ์ €์žฅํ•˜๋ฉฐ, localStorage๋Š” ๋น„๋ฏผ๊ฐ์„ฑ ๋ฐ์ดํ„ฐ์—๋งŒ ํ™œ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Why Wrong?

๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ(์˜ˆ: ์ธ์ฆ ํ† ํฐ, ์‚ฌ์šฉ์ž ID, ๊ฐœ์ธ ์ •๋ณด)๋ฅผ localStorage๋‚˜ sessionStorage์— ์ง์ ‘ ์ €์žฅํ•˜๋Š” ๊ฒƒ์€ XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์— ๋…ธ์ถœ๋˜์–ด ์‹ฌ๊ฐํ•œ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. HttpOnly ํ”Œ๋ž˜๊ทธ๊ฐ€ ์„ค์ •๋œ ์ฟ ํ‚ค์™€ ๋‹ฌ๋ฆฌ, localStorage์™€ sessionStorage์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก JavaScript์—์„œ ์†์‰ฝ๊ฒŒ ์ ‘๊ทผํ•˜๊ณ  ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•…์˜์ ์ธ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ฃผ์ž…๋˜๋ฉด ์ด ๋ฐ์ดํ„ฐ๋ฅผ ํƒˆ์ทจํ•˜๊ฑฐ๋‚˜ ๋ณ€์กฐํ•˜์—ฌ ์„ธ์…˜ ํ•˜์ด์žฌํ‚น, ๋ฐ์ดํ„ฐ ๋„๋‚œ, ๊ถŒํ•œ ์—†๋Š” ์ ‘๊ทผ ๋“ฑ์˜ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ์ด๋“ค ์Šคํ† ๋ฆฌ์ง€๋Š” ๋™๊ธฐ์ ์œผ๋กœ ์ž‘๋™ํ•˜์—ฌ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ์— ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ ‘๊ทผํ•  ๊ฒฝ์šฐ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ์ฐจ๋‹จํ•˜์—ฌ UI ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

How to Fix?

  1. ์ธ์ฆ ํ† ํฐ ๊ด€๋ฆฌ: ์ธ์ฆ ํ† ํฐ(์˜ˆ: JWT)์€ ํด๋ผ์ด์–ธํŠธ ์ธก JavaScript๊ฐ€ ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋Š” HttpOnly ํ”Œ๋ž˜๊ทธ๊ฐ€ ์„ค์ •๋œ ์ฟ ํ‚ค๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. HttpOnly ์ฟ ํ‚ค๋Š” XSS ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ํ† ํฐ์„ ๋ณดํ˜ธํ•˜๋Š” ๋ฐ ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ Secure ํ”Œ๋ž˜๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ HTTPS ์—ฐ๊ฒฐ์—์„œ๋งŒ ์ „์†ก๋˜๋„๋ก ํ•˜๊ณ , SameSite ์†์„ฑ์„ ์„ค์ •ํ•˜์—ฌ CSRF ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. 2. ๋ฏผ๊ฐํ•œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ: ๊ฐœ์ธ ์‹๋ณ„ ์ •๋ณด(PII)๋‚˜ ๊ธˆ์œต ์ •๋ณด์™€ ๊ฐ™์ด ์ง„์ •์œผ๋กœ ๋ฏผ๊ฐํ•œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์„ ์ตœ๋Œ€ํ•œ ํ”ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์„œ๋ฒ„ ์ธก์— ์ €์žฅํ•˜๊ณ , ์–ด์ฉ” ์ˆ˜ ์—†์ด ํด๋ผ์ด์–ธํŠธ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค๋ฉด ์ €์žฅ ์ „ ๊ฐ•๋ ฅํ•˜๊ฒŒ ์•”ํ˜ธํ™”ํ•˜๊ณ , ์‚ฌ์šฉ ํ›„ ์ฆ‰์‹œ ์‚ญ์ œํ•˜๋Š” ๋“ฑ์˜ ์กฐ์น˜๋ฅผ ์ทจํ•ด์•ผ ํ•˜์ง€๋งŒ, ์ด ๊ฒฝ์šฐ์—๋„ ์—ฌ์ „ํžˆ ๋ณด์•ˆ ์œ„ํ—˜์ด ์กด์žฌํ•จ์„ ์ธ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. 3. ๋น„๋ฏผ๊ฐ์„ฑ ๋ฐ์ดํ„ฐ ํ™œ์šฉ: localStorage๋Š” ์‚ฌ์šฉ์ž ํ…Œ๋งˆ ์„ค์ •, UI ๋ ˆ์ด์•„์›ƒ ์„ ํ˜ธ๋„, ๋น„ํ™œ์„ฑ ์บ์‹œ ๋ฐ์ดํ„ฐ์™€ ๊ฐ™์ด ๋ณด์•ˆ์— ๋ฏผ๊ฐํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ ์„ค์ •์ด๋‚˜ ์ž์ฃผ ์ ‘๊ทผํ•˜๋Š” ์ •์  ๋ฐ์ดํ„ฐ ์บ์‹ฑ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. 4. ๋Œ€์šฉ๋Ÿ‰ ๋ฐ ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ: ๋” ๋งŽ์€ ์ €์žฅ ์šฉ๋Ÿ‰๊ณผ ํŠธ๋žœ์žญ์…˜, ์ธ๋ฑ์‹ฑ๊ณผ ๊ฐ™์€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ํ•„์š”๋กœ ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฐ์ดํ„ฐ๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ž‘๋™ํ•˜๊ณ  ๋” ๋งŽ์€ ์ €์žฅ ๊ณต๊ฐ„์„ ์ œ๊ณตํ•˜๋Š” IndexedDB๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. IndexedDB ๋˜ํ•œ XSS ๊ณต๊ฒฉ์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ €์žฅํ•˜๋Š” ๋ฐ์ดํ„ฐ์˜ ๋ฏผ๊ฐ๋„๋ฅผ ํ•ญ์ƒ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Before Code (Bad)

// ๐Ÿšจ ๋ฏผ๊ฐํ•œ ์‚ฌ์šฉ์ž ํ† ํฐ์„ localStorage์— ์ €์žฅํ•˜๋Š” ์˜ˆ์‹œ (๋ณด์•ˆ ์ทจ์•ฝ)
function login(username, password) {
  fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password })
  })
  .then(response => response.json())
  .then(data => {
    if (data.token) {
      localStorage.setItem('authToken', data.token); // ๐Ÿšจ XSS ๊ณต๊ฒฉ์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ๋Š” ํ† ํฐ ์ €์žฅ
      localStorage.setItem('userId', data.userId);   // ๐Ÿšจ XSS ๊ณต๊ฒฉ์— ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์šฉ์ž ID ์ €์žฅ
      console.log('๋กœ๊ทธ์ธ ์„ฑ๊ณต! ํ† ํฐ ๋ฐ ์‚ฌ์šฉ์ž ID๊ฐ€ localStorage์— ์ €์žฅ๋จ.');
    } else {
      console.error('๋กœ๊ทธ์ธ ์‹คํŒจ.');
    }
  })
  .catch(error => console.error('๋„คํŠธ์›Œํฌ ์—๋Ÿฌ:', error));
}

// ์ €์žฅ๋œ ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ API ์š”์ฒญ
function fetchData() {
  const token = localStorage.getItem('authToken'); // localStorage์—์„œ ํ† ํฐ์„ ์ง์ ‘ ์ฝ์–ด์˜ด
  if (token) {
    fetch('/api/data', {
      headers: { 'Authorization': `Bearer ${token}` }
    })
    .then(response => response.json())
    .then(data => console.log('๋ฐ์ดํ„ฐ:', data))
    .catch(error => console.error('๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์—๋Ÿฌ:', error));
  } else {
    console.warn('ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.');
  }
}

// ์˜ˆ์‹œ ์‚ฌ์šฉ (์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ถŒ์žฅ๋˜์ง€ ์•Š์Œ)
// login('testuser', 'testpass');
// fetchData();

After Code (Good)

// โœ… ์•ˆ์ „ํ•œ ์ธ์ฆ ํ† ํฐ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ HttpOnly ์ฟ ํ‚ค ์‚ฌ์šฉ ์˜ˆ์‹œ
// (๋ฐฑ์—”๋“œ์—์„œ HttpOnly, Secure, SameSite=Lax ์†์„ฑ์„ ๊ฐ€์ง„ Set-Cookie ํ—ค๋”๋ฅผ ์„ค์ •ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •)

// ํด๋ผ์ด์–ธํŠธ์—์„œ ๋กœ๊ทธ์ธ ์š”์ฒญ: ์„œ๋ฒ„๊ฐ€ HttpOnly ์ฟ ํ‚ค๋กœ ํ† ํฐ์„ ์„ค์ •ํ•จ
function loginSecure(username, password) {
  fetch('/api/login', { 
    method: 'POST',
    credentials: 'include', // ์š”์ฒญ ์‹œ ์ž๋™์œผ๋กœ ์ฟ ํ‚ค๋ฅผ ํฌํ•จํ•˜์—ฌ ์ „์†ก
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password })
  })
  .then(response => {
    if (response.ok) {
      console.log('๋กœ๊ทธ์ธ ์„ฑ๊ณต! ํ† ํฐ์€ HttpOnly ์ฟ ํ‚ค๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
      // ๋ฏผ๊ฐํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ ์„ค์ •์€ localStorage์— ์ €์žฅ ๊ฐ€๋Šฅ
      localStorage.setItem('userTheme', 'dark'); // โœ… ๋น„๋ฏผ๊ฐ์„ฑ ๋ฐ์ดํ„ฐ
      localStorage.setItem('lastLoginTime', new Date().toISOString()); // โœ… ๋น„๋ฏผ๊ฐ์„ฑ ๋ฐ์ดํ„ฐ
    } else {
      console.error('๋กœ๊ทธ์ธ ์‹คํŒจ.');
    }
  })
  .catch(error => console.error('๋„คํŠธ์›Œํฌ ์—๋Ÿฌ:', error));
}

// HttpOnly ์ฟ ํ‚ค๋Š” ํด๋ผ์ด์–ธํŠธ JavaScript์—์„œ ์ง์ ‘ ์ฝ์„ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ,
// API ์š”์ฒญ ์‹œ ์ž๋™์œผ๋กœ ํ•จ๊ป˜ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.
function fetchDataSecure() {
  fetch('/api/data', {
    credentials: 'include' // ์š”์ฒญ ์‹œ ์ž๋™์œผ๋กœ HttpOnly ์ฟ ํ‚ค๋ฅผ ํฌํ•จํ•˜์—ฌ ์ „์†ก
  })
  .then(response => response.json())
  .then(data => console.log('๋ฐ์ดํ„ฐ:', data))
  .catch(error => console.error('๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์—๋Ÿฌ:', error));
}

// ๋ฏผ๊ฐํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ํ™˜๊ฒฝ ์„ค์ • ์ €์žฅ ์˜ˆ์‹œ (localStorage ์‚ฌ์šฉ)
function saveUserPreference(setting, value) {
  try {
    localStorage.setItem(setting, value);
    console.log(`${setting} ์„ค์ •์ด localStorage์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`);
  } catch (e) {
    console.error(`localStorage ์ €์žฅ ์‹คํŒจ (${setting}):`, e);
  }
}

// ์˜ˆ์‹œ ์‚ฌ์šฉ
// loginSecure('secureuser', 'securepass');
// fetchDataSecure();
// saveUserPreference('notificationEnabled', 'true');