๐ XSS๋ ๋ฌด์์ธ๊ฐ?
ํฌ๋ก์ค ์ฌ์ดํธ ์คํฌ๋ฆฝํ (Cross-Site Scripting, ์ดํ XSS)์ ์น ๋ณด์ ์ทจ์ฝ์ ์ค ํ๋๋ก, ๊ณต๊ฒฉ์๊ฐ ์ฌ์ฉ์์ ์น ๋ธ๋ผ์ฐ์ ์ ์ ์์ ์ธ ์คํฌ๋ฆฝํธ๋ฅผ ์ฃผ์ ํ ์ ์๊ฒ ํ๋ ๊ณต๊ฒฉ ๋ฐฉ์์ ๋๋ค.
์ด ์ทจ์ฝ์ ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ์ ์ ๋ ฅ์ ์ ์ ํ ๊ฒ์ฆํ๊ฑฐ๋ ์ด์ค์ผ์ดํ ์ฒ๋ฆฌํ์ง ์์ ๋ ๋ฐ์ํฉ๋๋ค. XSS ๊ณต๊ฒฉ์ ์ฌ์ฉ์์ ์ธ์ ํ ํฐ, ์ฟ ํค, ๊ฐ์ธ ์ ๋ณด ๋ฑ์ ํ์ทจํ๊ฑฐ๋ ์ฌ์ฉ์๋ฅผ ๋์ ํ์ฌ ํน์ ํ์๋ฅผ ์ํํ๊ฒ ๋ง๋๋ ๋ฑ ๋ค์ํ ํํ๋ก ์ด๋ฃจ์ด์ง ์ ์์ต๋๋ค.
๐ XSS๋ ์ ๋ฑ์ฅํ๋๊ฐ?
XSS ์ทจ์ฝ์ ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐ์ ๊ณผ ํจ๊ป ๋ฑ์ฅํ์ต๋๋ค. ์ด๊ธฐ ์น ์ฌ์ดํธ๋ค์ ์ฃผ๋ก ์ ์ ์ธ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ๋ฐ ์ด์ ์ ๋ง์ท์ง๋ง, ์๊ฐ์ด ์ง๋๋ฉด์ ์ฌ์ฉ์ ์ํธ์์ฉ๊ณผ ๊ฐ์ธํ๋ ์ฝํ ์ธ ์ ํ์์ฑ์ด ์ฆ๊ฐํ์ต๋๋ค. ์ด๋ฌํ ๋ณํ๋ก ์ฌ์ฉ์ ์ ๋ ฅ์ ๋ฐ์์ ์ฒ๋ฆฌํ๋ ๊ธฐ๋ฅ์ด ํ์๊ฐ ๋์๊ณ , ์ด ๊ณผ์ ์์ ์ ๋ ฅ๊ฐ์ ๋ํ ์ถฉ๋ถํ ๊ฒ์ฆ๊ณผ ์ ์ ํ ์ฒ๋ฆฌ๊ฐ ์ด๋ฃจ์ด์ง์ง ์์ XSS์ ๊ฐ์ ์ทจ์ฝ์ ์ด ๋ฐ์ํ๊ฒ ๋์์ต๋๋ค.
๐ XSS ๊ณต๊ฒฉ์ ์๋ ์๋ฆฌ
โ๏ธ ๋น ์ง์์ ๊ธฐ๋ฒ (non-persistent)
์ฌ์ฉ์๊ฐ ํน์ ํ ๋งํฌ๋ฅผ ํด๋ฆญํ ๋ ๋ฐ์ํฉ๋๋ค. ๋งํฌ์๋ ์ ์์ ์ธ ์คํฌ๋ฆฝํธ๊ฐ ํฌํจ๋์ด ์์ผ๋ฉฐ, ์ฌ์ฉ์๊ฐ ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด ์คํฌ๋ฆฝํธ๋ฅผ ์น ํ์ด์ง์ ๋ฐ์ํด ์ฌ์ฉ์์ ๋ธ๋ผ์ฐ์ ์์ ์คํํฉ๋๋ค.
http://example.com/?query=<script>alert('์
์์ ์ธ ์คํฌ๋ฆฝํธ')</script>
โ๏ธ ์ง์์ ๊ธฐ๋ฒ (persistent)
์ ์์ ์ธ ์คํฌ๋ฆฝํธ๊ฐ ์น ์๋ฒ์ ์ ์ฅ๋๋ ๊ฒฝ์ฐ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ ๋๊ธ, ๊ฒ์๋ฌผ, ์ฌ์ฉ์ ํ๋กํ ๋ฑ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ฝ์ ํ๋ฉด, ์ด ์คํฌ๋ฆฝํธ๋ ๋ค๋ฅธ ์ฌ์ฉ์๋ค์ด ํด๋น ์ฝํ ์ธ ๋ฅผ ๋ณผ ๋๋ง๋ค ์คํ๋ฉ๋๋ค.
<!DOCTYPE html>
<html>
<head>
<title>XSS ๊ณต๊ฒฉ ์๋ฎฌ๋ ์ด์
</title>
</head>
<body>
<h1>๋๊ธ ์น์
</h1>
<div id="comments">
<!-- ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ๋๊ธ -->
<p>์ฌ์ฉ์ ๋๊ธ:
<script>alert('XSS ๊ณต๊ฒฉ์
๋๋ค!');</script>
</p>
</div>
</body>
</html>
โ๏ธ DOM based XSS
DOM ๊ธฐ๋ฐ XSS๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ํด๋ผ์ด์ธํธ ์ธก ์ฝ๋์์ ๋ฐ์ํฉ๋๋ค. ์ด ๊ฒฝ์ฐ, ์ ์์ ์ธ ์คํฌ๋ฆฝํธ๋ ํ์ด์ง ์์ฒด์๋ ์ ์ฅ๋์ง ์์ง๋ง, ํ์ด์ง์ DOM์ ์กฐ์ํ์ฌ ์คํ๋ฉ๋๋ค.
// URL์ ํ๋ผ๋ฏธํฐ๋ฅผ ํตํด ์
๋ ฅ๋ ๊ฐ์ ํ์ด์ง์ ๋ฐ์
const input = window.location.href.split('input=')[1];
document.getElementById('output').innerHTML = decodeURIComponent(input);
๐ XSS ๊ณต๊ฒฉ ๋ฐฉ์ง ๋ฐฉ๋ฒ
โ๏ธ ์ด์ค์ผ์ดํ
<!DOCTYPE html>
<html>
<head>
<title>XSS ๋ฐฉ์ง ์์</title>
</head>
<body>
<form id="myForm">
<label for="userInput">์
๋ ฅ:</label>
<input type="text" id="userInput" name="userInput">
<button type="submit">์ ์ถ</button>
</form>
<div id="result"></div>
<script>
document.getElementById('myForm').addEventListener('submit', function(event) {
event.preventDefault();
// ์ฌ์ฉ์ ์
๋ ฅ ๊ฐ ๊ฐ์ ธ์ค๊ธฐ
var userInput = document.getElementById('userInput').value;
// XSS ๋ฐฉ์ง๋ฅผ ์ํ ์ด์ค์ผ์ดํ
var safeInput = escapeHtml(userInput);
// ๊ฒฐ๊ณผ๋ฅผ ์์ ํ๊ฒ ํ์ด์ง์ ํ์
document.getElementById('result').innerText = safeInput;
});
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
</script>
</body>
</html>
โ๏ธ ์ฌ์ฉ์ ์ ๋ ฅ ๊ฒ์ฆ
function validateInput(input) {
let pattern = /<script.*?>.*?<\/script>/gi;
return input.replace(pattern, '');
}
โ๏ธ CSP ํค๋ ์ค์
CSP(Content-Security-Policy) ํค๋๋ ์น ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์ ๋ณด๋ด๋ HTTP ์๋ต ํค๋ ์ค ํ๋๋ก, ์น ํ์ด์ง์์ ์ฌ์ฉํ ์ ์๋ ๋ฆฌ์์ค์ ์ข ๋ฅ๋ฅผ ์ ํํฉ๋๋ค.
Content-Security-Policy: <policy-directive>; <policy-directive>
๋ค์์ ์์ ์ ์ฃผ์์ google.com์ ์คํฌ๋ฆฝํธ ๋ฆฌ์์ค๋ง ํ์ฉํ๋ ๊ฒ์ ์๋ฏธํ๋ค.
Content-Security-Policy: script-src 'self' *.google.com;
โ๏ธ Vue, React์ ๊ฐ์ ํ๋ ์์ํฌ ์ด์ฉํ๊ธฐ
๊ธฐ๋ณธ์ ์ผ๋ก Vue, React, Angular์ ๊ฐ์ ์น ํ๋ ์์ํฌ๋ ์ผ๋ถ XSS ๊ณต๊ฒฉ์ ๋ฐฉ์งํ๋ ๋ฉ์ปค๋์ฆ์ ์ ๊ณตํฉ๋๋ค.
์๋ ์ด์ค์ผ์ดํ
<h1>{{ userProvidedString }}</h1>
// ๊ฐ์ด '<script>alert("hi")</script>' ์ผ ๋
// ๋ค์๊ณผ ๊ฐ์ด ์ด์ค์ผ์ดํ ๋๋ค.
<script>alert("hi")</script>
๐ ์ฐธ๊ณ
์ํค๋ฐฑ๊ณผ - ์ฌ์ดํธ ๊ฐ ์คํฌ๋ฆฝํ
Cloudflare
CSP๋?
Vue Guide