ํ๋ก์ ํธ ์ฑ๊ธ ์ ์ฌ๋ฌ ๋๋ฐ์ด์ค ๋ทฐํฌํธ๋ฅผ ์บ๋ฒ์ค์ ๋ฐฐ์นํ๊ณ , ์ธ๋ถ ์น์ฌ์ดํธ๋ฅผ ๊ฐ ๋ทฐํฌํธ์ ๋์์ ๋ก๋ํ์ฌ ๋ฐ์ํ ๋์์ธ์ ๋น๊ตํ ์ ์๋ ํด์ ๋๋ค. ๊ฐ๋ฐ์ ์งํํ๋ฉด์ ํฌ๊ฒ ๋ ๊ฐ์ง ๊ธฐ์ ์ ๋์ ์ด ์์์ต๋๋ค.
์ฒซ ๋ฒ์งธ๋ ์ธ๋ถ ์น์ฌ์ดํธ๋ฅผ iframe ์์์ ์ด๋ป๊ฒ ๋ก๋ํ ๊ฒ์ธ๊ฐ ์์ต๋๋ค. ๋๋ถ๋ถ์ ์น์ฌ์ดํธ๋ ๋ณด์ ์ ์ฑ
์ผ๋ก iframe ๋ก๋ฉ์ ์ฐจ๋จํ๊ณ ์์ด, ๋จ์ํ <iframe src={url} /> ์ผ๋ก๋ ์๋ฌด๊ฒ๋ ๋ณด์ด์ง ์์ต๋๋ค.
๋ ๋ฒ์งธ๋ ์ฌ๋ฌ ๋ทฐํฌํธ ๊ฐ์ ์คํฌ๋กค์ ์ด๋ป๊ฒ ๋๊ธฐํํ ๊ฒ์ธ๊ฐ ์์ต๋๋ค. ํ๋ก์๋ฅผ ํตํด same-origin์ผ๋ก ์ ํํ๋๋ผ๋, ์ค์๊ฐ์ผ๋ก ๋ทฐํฌํธ ๊ฐ ์ํ๋ฅผ ์ฐ๋ํ๋ ๊ตฌ์กฐ๊ฐ ํ์ํ์ต๋๋ค.
์ด ๊ฒ์๊ธ์์๋ ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๊ตฌํํ HTTP ๋ฆฌ๋ฒ์ค ํ๋ก์ ์๋ฒ์ postMessage ๊ธฐ๋ฐ ์คํฌ๋กค ๋๊ธฐํ ๊ตฌ์กฐ๋ฅผ ์ ๋ฆฌํฉ๋๋ค.
๋ธ๋ผ์ฐ์ ๋ ์ธ๋ถ ์ฌ์ดํธ์ iframe ๋ก๋ฉ์ ๋ง๊ธฐ ์ํด ๋ ๊ฐ์ง ๋ณด์ ํค๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
DENY ๋๋ SAMEORIGIN ์ผ๋ก ์ค์ ๋ ๊ฒฝ์ฐ ํด๋น ์ฌ์ดํธ๋ iframe์์ ๋ ๋๋ง๋์ง ์์ต๋๋ค.frame-ancestors ์ง์์ด๋ก ํ์ฉ๋ ์ถ์ฒ๋ง iframe ์ฝ์
์ด ๊ฐ๋ฅํฉ๋๋ค.๋ํ iframe์ ์ถ์ฒ๊ฐ ๋ถ๋ชจ ํ์ด์ง์ ๋ค๋ฅผ ๊ฒฝ์ฐ, DOM ์ ๊ทผ๊ณผ ์คํฌ๋กค ์ด๋ฒคํธ ์์ ์ด cross-origin ์ ์ฑ ์ ์ํด ์ฐจ๋จ๋ฉ๋๋ค. ์คํฌ๋กค ๋๊ธฐํ๋ฅผ ๊ตฌํํ๋ ค๋ฉด iframe ๋ด๋ถ์ ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ๋ถ๋ชจ ํ์ด์ง๊ฐ ๊ฐ์งํ ์ ์์ด์ผ ํ๋๋ฐ, ์ด ๋ถ๋ถ์ด ํต์ฌ ๋ฌธ์ ์์ต๋๋ค.
๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์๋ฒ ์ธก ๋ฆฌ๋ฒ์ค ํ๋ก์๋ฅผ ๊ตฌ์ถํ์ต๋๋ค. ํด๋ผ์ด์ธํธ๋ ์ธ๋ถ URL์ ์ง์ ์ ๊ทผํ๋ ๋์ , ์์ฒด ์๋ฒ์ ํ๋ก์ ์๋ํฌ์ธํธ๋ฅผ ๊ฒฝ์ ํฉ๋๋ค.
๋ธ๋ผ์ฐ์ iframe
โ
โโ GET /api/proxy?url=https://example.com
โ
โผ
server.ts (HTTP ์๋ฒ)
โ pathname์ด "/api/proxy"๋ก ์์ํ๋ฉด
โ Next.js ๋์ ํ๋ก์ ํธ๋ค๋ฌ๋ก ๋ผ์ฐํ
โ
โผ
proxy.ts โ handleProxyRequest()
โ
โโ 1. URL ํ๋ผ๋ฏธํฐ ์ถ์ถ
โโ 2. SSRF ๋ณด์ ๊ฒ์ฆ
โโ 3. fetch()๋ก ์๊ฒฉ ์ฝํ
์ธ ๊ฐ์ ธ์ค๊ธฐ
โโ 4. ์ฐจ๋จ ํค๋ ์ ๊ฑฐ (x-frame-options, CSP)
โโ 5. Content-Type๋ณ ๋ถ๊ธฐ ์ฒ๋ฆฌ
โโ 6. ํด๋ผ์ด์ธํธ์ ์๋ต
ํด๋ผ์ด์ธํธ ์ธก์์๋ ๋จ ํ ์ค์ ๋ณ๊ฒฝ์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
tsx
// ๋ณ๊ฒฝ ์ : ์ง์ URL ๋ก๋ฉ (CORS/CSP ์ฐจ๋จ)
<iframe src={url} />
// ๋ณ๊ฒฝ ํ: ํ๋ก์ ๊ฒฝ์ (same-origin)
<iframe src={`/api/proxy?url=${encodeURIComponent(url)}`} />
ํ๋ก์๋ฅผ ํตํด ์ธ๋ถ ์ฝํ
์ธ ๊ฐ ์์ฒด ๋๋ฉ์ธ(/api/proxy)์ผ๋ก ์๋น๋๋ฏ๋ก, ๋ธ๋ผ์ฐ์ ์
์ฅ์์๋ same-origin ์์ฒญ์ด ๋ฉ๋๋ค. ์ด๋ก์จ ์ฐจ๋จ ํค๋๋ฅผ ์ ๊ฑฐํ ์ ์๊ณ , DOM๊ณผ ์คํฌ๋กค ์ด๋ฒคํธ์๋ ์ ๊ทผ์ด ๊ฐ๋ฅํด์ง๋๋ค.
ํ๋ก์ ์๋ฒ๋ ์ธ๋ถ์์ ์์์ URL์ ์ ๋ฌ๋ฐ์ ์๋ฒ๊ฐ ์์ฒญ์ ๋์ ๋ณด๋ด๋ ๊ตฌ์กฐ์ด๊ธฐ ๋๋ฌธ์, SSRF(Server-Side Request Forgery) ๊ณต๊ฒฉ์ ์ทจ์ฝํ ์ ์์ต๋๋ค. ๊ณต๊ฒฉ์๊ฐ http://192.168.1.1 ๊ฐ์ ๋ด๋ถ ๋คํธ์ํฌ ์ฃผ์๋ฅผ URL๋ก ์ ๋ฌํ๋ฉด, ์๋ฒ๊ฐ ๋ด๋ถ ์ธํ๋ผ์ ์ ๊ทผํ๋ ๊ฒฝ๋ก๋ก ์
์ฉ๋ ์ ์์ต๋๋ค.
์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด validateProxyUrl() ํจ์๋ฅผ ๊ตฌํํ์ต๋๋ค.
ts
async function validateProxyUrl(raw: string): Promise<URL | null> {
// 1. URL ํ์ฑ โ ์ ํจํ์ง ์์ผ๋ฉด null ๋ฐํ
// 2. ํ๋กํ ์ฝ ๊ฒ์ฌ โ http: ๋๋ https: ๋ง ํ์ฉ
// 3. ํธ์คํธ๋ช
๊ฒ์ฌ โ localhost ์ฐจ๋จ
// 4. DNS ์กฐํ โ ์ค์ IP ํ์ธ
// 5. ๋ด๋ถ IP ์ฐจ๋จ (127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x ๋ฑ)
}
URL ํ๋ผ๋ฏธํฐ๋ง ๊ฒ์ฌํ๋ ๊ฒ์ด ์๋๋ผ DNS ์กฐํ ํ ์ค์ IP๋ฅผ ํ์ธํ๋ ๊ฒ์ด ํต์ฌ์ ๋๋ค. DNS ๋ฆฌ๋ฐ์ธ๋ฉ ๊ณต๊ฒฉ์ฒ๋ผ, ์ธ๋ถ์ฒ๋ผ ๋ณด์ด๋ ๋๋ฉ์ธ์ด ๋ด๋ถ IP๋ฅผ ๊ฐ๋ฆฌํค๋ ๊ฒฝ์ฐ๊น์ง ์ฐจ๋จํ ์ ์์ต๋๋ค.
ํ๋ก์๋ฅผ ํตํด HTML์ ์๋นํ๋ฉด ์๋ก์ด ๋ฌธ์ ๊ฐ ์๊น๋๋ค. HTML ์์ ๋ฆฌ์์ค ๊ฒฝ๋ก๋ค(src, href, url() ๋ฑ)์ด ์๋ณธ ๋๋ฉ์ธ์ ๊ธฐ์ค์ผ๋ก ์์ฑ๋์ด ์๊ธฐ ๋๋ฌธ์, ํ๋ก์๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์๋ณธ ์๋ฒ์ ์ง์ ์์ฒญ์ ๋ณด๋ด๊ฒ ๋ฉ๋๋ค. ์ด๋ ๊ฒ ๋๋ฉด ๋ค์ CORS ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด HTML๊ณผ CSS ๋ด์ URL์ ํ๋ก์ ๊ฒฝ๋ก๋ก ์ฌ์์ฑํฉ๋๋ค.
์๋ณธ URL โ ๋ณํ ๊ฒฐ๊ณผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
data:, blob:, javascript:, # โ ๋ณํ ์ ํจ (์คํต)
//cdn.example.com/style.css โ /api/proxy?url=https://cdn.example.com/style.css
https://example.com/img.png โ /api/proxy?url=https://example.com/img.png
/assets/logo.svg โ /api/proxy?url=https://example.com/assets/logo.svg
./main.js โ /api/proxy?url=https://example.com/page/main.js
์ฌ์์ฑ ๋์์ HTML ์์ฑ(src, href, action, poster), srcset, ์ธ๋ผ์ธ ์คํ์ผ, <style> ํ๊ทธ, ๋ณ๋ CSS ํ์ผ์ url() ์ฐธ์กฐ์
๋๋ค.
ํ ๊ฐ์ง ์ฃผ์ํ ์ ์ด ์์์ต๋๋ค. ์ ๊ท์ ๊ธฐ๋ฐ ์ฌ์์ฑ์ <script> ๋ด๋ถ JavaScript ์ฝ๋๋ ํจํด์ ๋งค์นญ๋ ์ ์์ด, JS ์ฝ๋๊ฐ ์๋์น ์๊ฒ ์์๋ ์ํ์ด ์์ต๋๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์คํฌ๋ฆฝํธ ๋ณดํธ ๋ฐฉ์์ ์ ์ฉํ์ต๋๋ค.
โ <script>...</script> ๋ธ๋ก์ ํ๋ ์ด์คํ๋๋ก ์นํ (์ถ์ถ)
โก HTML ์์ฑ / CSS url() ๋ฆฌ๋ผ์ดํ
(์์ ํ ์์ญ๋ง)
โข ํ๋ ์ด์คํ๋๋ฅผ ์๋ <script> ๋ธ๋ก์ผ๋ก ๋ณต์ (๋ณต์)
๋ํ <base> ํ๊ทธ๋ ์ฌ์ฉํ์ง ์์์ต๋๋ค. <base href="https://example.com"> ์ ์ถ๊ฐํ๋ฉด ์๋ ๊ฒฝ๋ก ๋ฌธ์ ๋ ํด๊ฒฐ๋์ง๋ง, ์ด๋ฏธ ์ฌ์์ฑ๋ /api/proxy?url=... ๊ฒฝ๋ก๊ฐ <base> ํ๊ทธ์ ์ํด ์๋ณธ ๋๋ฉ์ธ์ผ๋ก ๋ค์ ํด์๋์ด CORS ์๋ฌ๋ฅผ ์ ๋ฐํ๊ธฐ ๋๋ฌธ์
๋๋ค.
์๋ต ํ์ ์ ๋ฐ๋ผ ์ฒ๋ฆฌ ๋ฐฉ์์ ๋ค๋ฅด๊ฒ ํ์ต๋๋ค.
| Content-Type | ์ฒ๋ฆฌ ๋ฐฉ์ |
| -------------------------- | --------------------------------------------- |
| text/html | ์ ์ฒด ์ฝ๊ธฐ โ URL ์ฌ์์ฑ + ์คํฌ๋ฆฝํธ ์ฃผ์
โ ์๋ต |
| text/css | ์ ์ฒด ์ฝ๊ธฐ โ url() ์ฐธ์กฐ ์ฌ์์ฑ โ ์๋ต |
| ๊ธฐํ (JS, ์ด๋ฏธ์ง, ํฐํธ ๋ฑ) | ์คํธ๋ฆฌ๋ฐ ํจ์ค์ค๋ฃจ |
JS, ์ด๋ฏธ์ง, ํฐํธ ๊ฐ์ ๋ฐ์ด๋๋ฆฌ ๋ฆฌ์์ค๋ ๋ณํ์ด ๋ถํ์ํ๋ฏ๋ก ์คํธ๋ฆฌ๋ฐ์ผ๋ก ๋ฐ๋ก ์ ๋ฌํฉ๋๋ค. HTML๊ณผ CSS๋ง ์ ์ฒด๋ฅผ ์ฝ์ด์ ์ฌ์์ฑ ํ ์๋ตํฉ๋๋ค.
ํ๋ก์๋ฅผ ํตํด same-origin ์ผ๋ก ์ ํ๋๋๋ผ๋, iframe ๋ด๋ถ์ ์ธ๋ถ๋ ์ฌ์ ํ ๋ณ๋์ window ์ปจํ
์คํธ์
๋๋ค. ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ๋ถ๋ชจ ํ์ด์ง๋ก ์ ๋ฌํ๋ ค๋ฉด postMessage API๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
ํ๋ก์๊ฐ HTML์ ์ฌ์์ฑํ ๋, </head> ์์ ๋ธ๋ฆฟ์ง ์คํฌ๋ฆฝํธ๋ฅผ ์ฃผ์
ํฉ๋๋ค.
js
// ์คํฌ๋กค ์ด๋ฒคํธ โ ๋ถ๋ชจ์ postMessage
window.addEventListener("scroll", () => {
window.parent.postMessage(
{
type: "proxy:scroll",
scrollX: window.scrollX,
scrollY: window.scrollY,
scrollHeight: document.documentElement.scrollHeight,
clientHeight: document.documentElement.clientHeight,
},
"*",
);
});
// ์ฝํ
์ธ ํฌ๊ธฐ ๋ณ๊ฒฝ ๊ฐ์ง
new ResizeObserver(() => {
window.parent.postMessage(
{
type: "proxy:resize",
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight,
},
"*",
);
}).observe(document.documentElement);
์ด ์คํฌ๋ฆฝํธ๊ฐ iframe ๋ด๋ถ์์ ์คํ๋๋ฉด์, ์คํฌ๋กค์ด ๋ฐ์ํ ๋๋ง๋ค ๋ถ๋ชจ ํ์ด์ง๋ก ์คํฌ๋กค ์์น์ ์ฝํ ์ธ ํฌ๊ธฐ๋ฅผ ์ ๋ฌํฉ๋๋ค.
์ถ๊ฐ๋ก history.pushState / replaceState ๋ฅผ ํจ์นํ์ฌ ํ๋ก์๋ ํ์ด์ง ๋ด๋ถ์์ SPA ๋ผ์ฐํ
์ด ๋ฐ์ํ ๋์ cross-origin SecurityError ๋ฅผ ๋ฐฉ์งํ์ต๋๋ค.
๋ถ๋ชจ ํ์ด์ง์ useScrollSync ํ
์์ message ์ด๋ฒคํธ๋ฅผ ์์ ํ์ฌ ๋ค๋ฅธ ๋ทฐํฌํธ์ ๋๊ธฐํํฉ๋๋ค.
ts
window.addEventListener("message", (event) => {
if (event.data.type === "proxy:scroll") {
// ์คํฌ๋กค ๋น์จ ๊ณ์ฐ ํ ๋ค๋ฅธ ๋ทฐํฌํธ์ ๋๊ธฐํ
}
});
์คํฌ๋กค ์์น๋ฅผ ์ ๋๊ฐ์ด ์๋ ๋น์จ๋ก ๋๊ธฐํํ๋ค๋ ์ ์ด ์ค์ํฉ๋๋ค. ๊ฐ ๋ทฐํฌํธ์ ์ฝํ ์ธ ๋์ด๊ฐ ๋ค๋ฅผ ์ ์๊ธฐ ๋๋ฌธ์, ์ ๋๊ฐ ๋๊ธฐํ๋ ๋ทฐํฌํธ๋ง๋ค ๋ค๋ฅธ ์์น๋ฅผ ๋ณด์ฌ์ฃผ๊ฒ ๋ฉ๋๋ค. ๋น์จ ๊ธฐ๋ฐ์ผ๋ก ์ฒ๋ฆฌํ๋ฉด ๋ชจ๋ ๋ทฐํฌํธ๊ฐ ์๋์ ์ผ๋ก ๊ฐ์ ์์น๋ฅผ ๋ณด์ฌ์ค ์ ์์ต๋๋ค.
์คํฌ๋กค ๋๊ธฐํ ์ธ์๋ Socket.IO ๊ธฐ๋ฐ์ผ๋ก ์ค์๊ฐ ํ์ ๊ธฐ๋ฅ์ ๊ตฌํํ์ต๋๋ค.
OWNER / EDITOR / VIEWER ์ธ ๊ฐ์ง ์ญํ ๋ก ๊ถํ์ ๊ด๋ฆฌํฉ๋๋ค. VIEWER๋ URL ๋ณ๊ฒฝ์ด๋ ๋ทฐํฌํธ ์กฐ์์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค.์ด ํ๋ก์ ํธ์์ ๊ฐ์ฅ ์ธ์ ๊น์๋ ๋ถ๋ถ์ ๋ฌธ์ ๊ฐ ์ฐ์์ ์ผ๋ก ์ด์ด์ง๋ค๋ ์ ์ด์์ต๋๋ค. iframe ์ฐจ๋จ์ ํด๊ฒฐํ๊ธฐ ์ํด ํ๋ก์๋ฅผ ๋ง๋ค์๊ณ , ํ๋ก์๋ฅผ ๋ง๋๋ URL ์ฌ์์ฑ ๋ฌธ์ ๊ฐ ์๊ฒผ๊ณ , URL ์ฌ์์ฑ์ ํ๋ JS ์ฝ๋ ์์ ๋ฌธ์ ๊ฐ ์๊ฒผ์ต๋๋ค. ํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ๋๋ง๋ค ๋ค์ ๋ฌธ์ ๊ฐ ๋๋ฌ๋๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ์ด ์งํ๋์ต๋๋ค.
๊ทธ ๊ณผ์ ์์ ๋ณด์์ ํญ์ ํจ๊ป ๊ณ ๋ คํด์ผ ํ๋ค๋ ์ ๋ ์์ผ ์ค๊ฐํ์ต๋๋ค. ๋จ์ํ ํ๋ก์๋ฅผ ์ด์ด๋๋ ๊ฒ๋ง์ผ๋ก๋ SSRF ์ทจ์ฝ์ ์ด ์๊ธฐ๊ณ , ์ด๋ฅผ ๋ง๊ธฐ ์ํด DNS ์กฐํ๊น์ง ํฌํจํ ๊ฒ์ฆ ๋ก์ง์ด ํ์ํ์ต๋๋ค.
postMessage์ ๋น์จ ๊ธฐ๋ฐ ์คํฌ๋กค ๋๊ธฐํ๋ ์๊ฐ๋ณด๋ค ๊น๋ํ ํด๊ฒฐ์ฑ ์ด์์ต๋๋ค. cross-origin ์ ์ฝ์ ํผํ๋ฉด์๋ ์ค์๊ฐ ๋๊ธฐํ๋ฅผ ๊ตฌํํ ์ ์์๊ณ , ๋น์จ ๊ธฐ๋ฐ ์ฒ๋ฆฌ ๋๋ถ์ ๋ทฐํฌํธ ํฌ๊ธฐ๊ฐ ๋ฌ๋ผ๋ ์ผ๊ด๋ ๋๊ธฐํ๊ฐ ๊ฐ๋ฅํ์ต๋๋ค.