๐ก๏ธ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ํด๋ผ์ด์ธํธ ์คํ ๋ฆฌ์ง ์ค๋จ์ฉ: XSS ๊ณต๊ฒฉ์ ๋จน์๊ฐ
Summary
๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ localStorage
๋ sessionStorage
์ ์ง์ ์ ์ฅํ๋ ๊ฒ์ XSS ๊ณต๊ฒฉ์ ์ทจ์ฝํ์ฌ ์ฌ๊ฐํ ๋ณด์ ์ํ์ ์ด๋ํฉ๋๋ค. ์ธ์ฆ ํ ํฐ์ HttpOnly
์ฟ ํค๋ฅผ ์ฌ์ฉํ๊ณ , ๋ฏผ๊ฐํ ์ฌ์ฉ์ ์ ๋ณด๋ ํด๋ผ์ด์ธํธ ์ธก์ ์ ์ฅํ์ง ์๊ฑฐ๋ ์ํธํํ์ฌ ์ต์ํ์ ํ์ํ ์ ๋ณด๋ง ์ ์ฅํ๋ฉฐ, localStorage
๋ ๋น๋ฏผ๊ฐ์ฑ ๋ฐ์ดํฐ์๋ง ํ์ฉํด์ผ ํฉ๋๋ค.
Why Wrong?
๋ฏผ๊ฐํ ๋ฐ์ดํฐ(์: ์ธ์ฆ ํ ํฐ, ์ฌ์ฉ์ ID, ๊ฐ์ธ ์ ๋ณด)๋ฅผ localStorage
๋ sessionStorage
์ ์ง์ ์ ์ฅํ๋ ๊ฒ์ XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์ ๋
ธ์ถ๋์ด ์ฌ๊ฐํ ๋ณด์ ์ทจ์ฝ์ ์ ๋ง๋ญ๋๋ค. HttpOnly
ํ๋๊ทธ๊ฐ ์ค์ ๋ ์ฟ ํค์ ๋ฌ๋ฆฌ, localStorage
์ sessionStorage
์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ ํด๋ผ์ด์ธํธ ์ธก JavaScript์์ ์์ฝ๊ฒ ์ ๊ทผํ๊ณ ์์ ํ ์ ์์ต๋๋ค. ์
์์ ์ธ ์คํฌ๋ฆฝํธ๊ฐ ์ฃผ์
๋๋ฉด ์ด ๋ฐ์ดํฐ๋ฅผ ํ์ทจํ๊ฑฐ๋ ๋ณ์กฐํ์ฌ ์ธ์
ํ์ด์ฌํน, ๋ฐ์ดํฐ ๋๋, ๊ถํ ์๋ ์ ๊ทผ ๋ฑ์ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํ ์ ์์ต๋๋ค. ๋ํ, ์ด๋ค ์คํ ๋ฆฌ์ง๋ ๋๊ธฐ์ ์ผ๋ก ์๋ํ์ฌ ๋๋์ ๋ฐ์ดํฐ์ ๋น๋ฒํ๊ฒ ์ ๊ทผํ ๊ฒฝ์ฐ ๋ฉ์ธ ์ค๋ ๋๋ฅผ ์ฐจ๋จํ์ฌ UI ์ฑ๋ฅ ์ ํ๋ฅผ ์ผ์ผํฌ ์ ์์ต๋๋ค.
How to Fix?
- ์ธ์ฆ ํ ํฐ ๊ด๋ฆฌ: ์ธ์ฆ ํ ํฐ(์: 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');