내 사이트가 피싱 사이트의 제물이 되었다 (보안 헤더 완벽 가이드)
1. 섬뜩한 발견 - "이거 님 사이트 아니에요?"
로그를 분석하다가 이상한 Referrer(유입 경로)를 발견했습니다.
fake-event-winner.com이라는 곳에서 트래픽이 쏟아지고 있었습니다.
접속해보니, 제 웹사이트가 그대로 떠 있었습니다.
알고 보니 제 사이트 위에 투명한 버튼을 씌워서, 사용자가 "이벤트 응모" 버튼을 누르면 실제로는 제 사이트의 "송금" 버튼이 눌리게 만드는 클릭재킹(Clickjacking) 공격이었습니다.
소스코드를 보니 <iframe> 태그 하나로 제 사이트를 가두고 있었습니다.
"아니, 남의 사이트를 이렇게 허락도 없이 가져다 쓸 수 있다고?"
네, 가능합니다. 여러분이 서버에서 "보안 헤더(Security Headers)"를 설정하지 않았다면요.
2. 보안 헤더 - 브라우저에 내리는 "방어 명령서"
최신 브라우저(Chrome, Safari 등)는 강력한 보안 기능을 내장하고 있습니다. 하지만 이 기능들은 기본적으로 꺼져 있습니다. (하위 호환성 때문이죠.) 서버가 응답 헤더(Response Header)를 통해 "이 기능 켜!"라고 명령해야만 작동합니다.
필수 보안 헤더 6가지를 소개합니다.
1) Strict-Transport-Security (HSTS): "HTTPS 아니면 안 받아"
사용자가 주소창에 http://naver.com이라고 쳐도, 브라우저가 강제로 https://로 바꿔서 접속하게 합니다.
중간자 공격(MITM)을 원천 차단합니다.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
2) X-Frame-Options: "날 가두지 마 (클릭재킹 방지)"
가장 중요합니다. 다른 사이트가 내 페이지를 <iframe> 안에 넣는 것을 막습니다.
X-Frame-Options: DENY
# 또는
X-Frame-Options: SAMEORIGIN
- DENY: 누구도 나를 iframe에 넣을 수 없음.
- SAMEORIGIN: 같은 도메인(내 사이트)에서만 허용.
3) X-Content-Type-Options: "시키는 대로만 해"
일명 "MIME Sniffing 차단" 헤더입니다.
해커가 .jpg 파일 확장자를 가진 악성 스크립트 파일을 업로드하고, 브라우저가 이걸 스크립트로 실행하게 만드는 공격을 막습니다.
X-Content-Type-Options: nosniff
"내가 CSS라고 줬으면 CSS로만 읽어. 멋대로 추측해서 실행하지 마."
4) Referrer-Policy: "내가 어디서 왔는지 비밀이야"
사용자가 링크를 타고 다른 사이트로 이동할 때, "이전 사이트 주소"를 얼마나 알려줄지 결정합니다. 개인정보 보호에 중요합니다.
Referrer-Policy: strict-origin-when-cross-origin
5) Permissions-Policy: "카메라/마이크 끄기"
브라우저의 기능(Feature)을 제한합니다. 내 웹사이트는 지도 앱이 아닌데 위치 정보(Geolocation)나 마이크를 켤 이유가 없죠? 미리 차단해두면 해킹당해도 안전합니다.
Permissions-Policy: geolocation=(), microphone=()
6) Content-Security-Policy (CSP): "끝판왕"
가장 강력하지만 설정하기 까다롭습니다. XSS를 막기 위해 로드할 수 있는 스크립트, 이미지, 스타일의 출처를 화이트리스트로 관리합니다. (자세한 건 XSS 편 참고)
3. 적용 - 어떻게 설정하나요?
방법 1 - Nginx (추천)
웹 서버 앞단에서 설정하는 게 가장 깔끔합니다.
# nginx.conf
server {
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin";
}
방법 2: Express (Node.js)
Helmet 라이브러리를 쓰면 한 줄로 끝납니다.
const helmet = require('helmet');
app.use(helmet());
// 끝. 주요 헤더 15개가 자동 적용됨.
방법 3: Next.js
next.config.js에서 설정합니다.
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// ...
],
},
];
},
};
4. 점수 확인 - 내 사이트는 몇 점?
설정을 마쳤다면 securityheaders.com 에 접속해서 여러분의 사이트 주소를 입력해보세요.
- A+ 등급: 완벽합니다.
- F 등급: 지금 당장 서버 설정 파일을 여세요.
대부분의 기본 설정 웹사이트는 D 또는 F가 나옵니다. 해커들에게 "어서 오세요"라고 대문을 열어둔 셈입니다.
5. 마무리 - 5분 투자로 5억 막기
보안 헤더 설정은 복잡한 코딩이 필요 없습니다. 설정 파일 몇 줄만 고치면 됩니다. 하지만 그 효과는 엄청납니다.
- 클릭재킹 방지
- MIME Sniffing 방지
- HTTPS 강제
- XSS 완화
이 모든 게 헤더 몇 줄로 해결됩니다. 지금 당장 여러분의 사이트를 검사해보세요. "보이지 않는 방패"가 튼튼한지 확인하세요.
6. 팁 - 지워야 할 헤더도 있다 (X-Powered-By)
보안을 위해 추가해야 할 헤더도 있지만, 삭제해야 할 헤더도 있습니다.
바로 X-Powered-By와 Server 헤더입니다.
X-Powered-By: ExpressServer: nginx/1.18.0
이 헤더들은 해커에게 "나 Express 쓰고, Nginx 1.18 버전 써요"라고 광고하는 꼴입니다. 해커는 해당 버전의 취약점(CVE)을 검색해서 공격합니다. 그러니 반드시 숨기세요.
app.disable('x-powered-by'); // Express
server_tokens off; // Nginx
정보를 숨기는 것(Security through obscurity)이 완벽한 보안은 아니지만, 굳이 해커에게 지도를 쥐여줄 필요는 없습니다.
My Site Was Cloned by Phishers (The Ultimate Security Headers Guide)
1. The Creepy Discovery: "Is this your site?"
While analyzing logs, I noticed a strange Referrer URL.
Traffic was pouring in from fake-event-winner.com.
I visited the URL and saw my website exactly as it is.
It was a Clickjacking attack. They overlaid a transparent button on top of my site using CSS. When users clicked "Claim Prize" on their site, they were actually clicking "Transfer Money" on my site loaded inside an invisible iframe.
Looking at the source code, a single <iframe> tag had trapped my site.
"Wait, anyone can just embed my site like this without permission?"
Yes. Unless you configure "Security Headers" on your server.
2. Security Headers: Defense Orders for the Browser
Modern browsers (Chrome, Safari, etc.) have powerful built-in security features. But these features are off by default to maintain backward compatibility. The server must send specific Response Headers to say "Turn this ON!"
Here are the 6 Essential Security Headers you must know.
1) Strict-Transport-Security (HSTS): "HTTPS Only"
Even if a user types http://, the browser forces the connection to https://.
It prevents Man-in-the-Middle (MITM) attacks (e.g., stripping SSL at a public Wi-Fi).
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
2) X-Frame-Options: "Don't Cage Me (Anti-Clickjacking)"
Most important. It stops other sites from embedding your page in <iframe>, <frame>, or <object>.
X-Frame-Options: DENY
# or
X-Frame-Options: SAMEORIGIN
- DENY: No one can frame me.
- SAMEORIGIN: Only my domain can frame me. (Common choice)
3) X-Content-Type-Options: "Don't Guess"
Stops MIME Sniffing.
Prevents attacks where a hacker uploads a malicious file disguised as an image (.jpg), and the browser tries to "guess" and execute it as a script.
X-Content-Type-Options: nosniff
"If I said it's an image, treat it as an image. Don't try to run it."
4) Referrer-Policy: "My Origin is Private"
Controls how much information is passed in the Referer header when a user clicks a link to leave your site.
Referrer-Policy: strict-origin-when-cross-origin
Prevents leaking private URLs (like /reset-password?token=...) to third-party analytics.
5) Permissions-Policy: "Disable Camera & Mic"
Limits browser features. If your blog doesn't need the Camera, Microphone, or Geolocation, block them. Even if you get XSS'd, the hacker can't spy on the user.
Permissions-Policy: geolocation=(), microphone=()
6) Content-Security-Policy (CSP): "The Boss"
The most powerful header. It defines a whitelist of trusted sources for scripts, styles, and images. It creates a sandbox that prevents XSS. (See the XSS article for details).
3. Implementation: The Code
Method 1: Nginx (Recommended)
Configuring at the Web Server level is the most robust way.
# nginx.conf inside server block
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin";
Method 2: Express (Node.js)
Use the Helmet middleware. It's the industry standard.
const helmet = require('helmet');
app.use(helmet());
// Done. It automatically sets 15+ security headers.
Method 3: Next.js
Configure in next.config.js.
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// ...
],
},
];
},
};
4. Verification: What's Your Score?
After configuring, go to securityheaders.com and scan your URL.
- Grade A+: Perfect. You are safe.
- Grade F: Open your server config NOW.
Most default deployments score a D or F. They are practically inviting hackers in.
5. Conclusion: 5 Minutes to Save Millions
Security headers are not rocket science. They are just a few lines of configuration. But their impact is massive.
- Stops Clickjacking.
- Stops MIME Sniffing.
- Enforces HTTPS.
- Mitigates XSS.
All of this for free. Check your site today. Ensure your "Invisible Shield" is active.
6. Advanced: The Future of Headers
Security headers are evolving. Here are two emerging ones:
1) Feature-Policy (Renamed to Permissions-Policy)
We briefly mentioned this, but it's powerful. You can control:
accelerometercamerageolocationgyroscopemagnetometermicrophonepaymentusb
By explicitly disabling these (=()), you reduce the attack surface. Even if a hacker injects malicious JS, they cannot activate the webcam.
2) Expect-CT (Certificate Transparency)
Prevents the use of misissued SSL certificates. It tells the browser to check if the certificate appears in public CT logs.
Expect-CT: max-age=86400, enforce, report-uri="https://..."
If a rogue CA issues a fake certificate for your domain, this header ensures browsers reject it.
8. HSTS Preload List
Setting Strict-Transport-Security header helps, but the very first connection might still be HTTP (and vulnerable).
To fix this, you can submit your domain to the Chrome HSTS Preload List (hstspreload.org).
Once accepted, your domain is hardcoded into Chrome/Firefox/Safari as "HTTPS Only".
Even the first connection will be secure. Warning: Undoing this is very difficult and takes months to propagate.
9. Hidden Gem: Cache-Control for Security
We usually think of Cache-Control for performance, but it's vital for security too.
If your app displays sensitive data (e.g., Bank Balance, Medical Record), you MUST prevent shared computers (Internet Cafe) or proxies from caching it.
Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate
Pragma: no-cache
Expires: 0
This ensures that when the user logs out, going "Back" in the browser doesn't show the sensitive page again.
10. Deep Dive: Referrer-Policy Options
Why did I recommend strict-origin-when-cross-origin?
- no-referrer: Never send referrer. Good for privacy, bad for analytics (you won't know traffic sources).
- origin: Sends domain only (
https://site.com), stripping the path. - unsafe-url: Sends full URL always. Dangerous. Leaks password reset tokens or PII in URL.
- strict-origin-when-cross-origin (Default):
- Same Origin (Internal link): Send full path.
- Cross Origin (HTTPS -> HTTPS): Send domain only.
- Downgrade (HTTPS -> HTTP): Send nothing. This balances Privacy, Security, and Analytics perfectly.