오리진(Origin)은 웹 브라우저가 리소스의 출처를 구별하는 핵심 보안 개념입니다. 일반적인 HTTP(S) URL에서 오리진은 스킴(Scheme) + 호스트(Host) + 포트(Port)로 결정됩니다. 이 글에서는 정의부터 Same-Origin Policy, CORS, JavaScript, ASP.NET Core 및 WordPress 연동까지 실제 코드로 설명합니다.
1. URL과 Origin의 차이
https://csharp.ihavenomoney.co.kr:443/tutorial?id=10#intro
└https┘ └──────호스트──────────┘ └포트┘ └경로/쿼리/프래그먼트┘
이 URL의 오리진은 https://csharp.ihavenomoney.co.kr입니다. HTTPS 기본 포트 443은 표시에서 생략될 수 있습니다. HTTP 기본 포트는 80입니다. 경로, 쿼리 문자열, 프래그먼트, 사용자 정보는 오리진에 포함되지 않습니다. 여기서 ‘프로토콜’보다 URL 표준 용어인 ‘스킴’이 더 정확하며, 호스트는 도메인 이름뿐 아니라 IP 주소일 수도 있습니다.
2. 동일 오리진 비교표
기준: https://csharp.ihavenomoney.co.kr
| 비교 주소 | 동일 오리진? | 이유 |
|---|---|---|
https://csharp.ihavenomoney.co.kr/post/1 | 예 | 경로만 다름 (80, 443 동일로 간주) |
https://csharp.ihavenomoney.co.kr:443 | 예 | 기본 포트 (80, 443 동일로 간주) |
http://csharp.ihavenomoney.co.kr | 아니요 | 스킴 다름 |
https://ihavenomoney.co.kr | 아니요 | 호스트 다름 |
https://csharp.ihavenomoney.co.kr:8443 | 아니요 | 포트 다름 |
같은 서버 IP를 사용하거나 같은 상위 도메인에 속하더라도 반드시 같은 오리진은 아닙니다.
3. Same-Origin Policy(SOP)란?
동일 출처 정책은 한 출처에서 실행된 스크립트가 다른 출처의 민감한 응답 데이터나 DOM에 무제한 접근하지 못하게 하는 브라우저 보안 규칙입니다. 예를 들어 악성 사이트가 사용자의 로그인 세션을 이용해 다른 사이트의 개인정보를 마음대로 읽지 못하게 제한합니다.
단, 교차 출처 요청이 모두 금지되는 것은 아닙니다. 링크 이동, 일부 HTML 폼 전송, 이미지·스크립트 삽입 등은 별도 규칙에 따라 가능하며, fetch()의 교차 출처 응답을 JavaScript가 읽는 행위는 일반적으로 CORS 허가가 필요합니다. CORS는 인증이나 CSRF 방어를 대신하지 않습니다.
4. Origin HTTP 헤더와 Referer의 차이
Origin: https://csharp.ihavenomoney.co.kr
Referer: https://csharp.ihavenomoney.co.kr/tutorial?id=10
Origin은 요청을 시작한 출처만 표현하며 경로를 포함하지 않습니다. Referer는 리퍼러 정책에 따라 경로 등의 정보가 포함되거나 축소·생략될 수 있습니다. 모든 요청에 Origin 헤더가 반드시 포함되는 것은 아니며, 샌드박스나 불투명 출처 등에서는 Origin: null이 나타날 수 있습니다.
5. CORS는 어떻게 작동하나?
다른 오리진에서 API를 호출할 때 브라우저는 응답의 Access-Control-Allow-Origin 등을 검사합니다. 서버가 허용한 출처와 일치하면 브라우저 JavaScript가 응답을 읽을 수 있습니다.
Access-Control-Allow-Origin: https://csharp.ihavenomoney.co.kr
Vary: Origin
서버가 요청 Origin에 따라 허용 값을 동적으로 반환한다면 캐시 혼동을 피하기 위해 Vary: Origin을 고려해야 합니다. 공개 데이터에는 Access-Control-Allow-Origin: *이 가능하지만, 쿠키 등 자격 증명을 포함하는 요청의 응답에는 와일드카드를 사용할 수 없습니다.
Preflight(사전 요청)
특정 메서드 또는 비단순 요청 헤더를 사용하면 브라우저는 실제 요청에 앞서 OPTIONS 요청을 보내 허용 여부를 확인합니다. 모든 교차 출처 요청에 Preflight가 발생하는 것은 아닙니다.
OPTIONS /api/items HTTP/1.1
Origin: https://csharp.ihavenomoney.co.kr
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
서버는 필요한 경우 Access-Control-Allow-Methods, Access-Control-Allow-Headers 등을 반환합니다. 이 검사가 통과하면 브라우저가 실제 요청을 진행합니다.
6. JavaScript에서 현재 Origin 확인
console.log(window.location.origin);
// 예: https://csharp.ihavenomoney.co.kr
const target = new URL('https://ihavenomoney.co.kr/wp-json/wp/v2/posts');
console.log(target.origin);
console.log(target.origin === window.location.origin); // false
URL.origin은 브라우저가 정규화한 출처를 확인할 때 편리합니다. 단, file:, data: 등 특수 스킴은 일반적인 HTTP(S) 오리진과 다르게 취급됩니다.
7. JavaScript fetch() 교차 출처 요청
async function loadPosts() {
const response = await fetch('https://ihavenomoney.co.kr/wp-json/wp/v2/posts?per_page=3');
if (!response.ok) throw new Error('HTTP ' + response.status);
const posts = await response.json();
console.log(posts);
}
loadPosts().catch(console.error);
위 코드는 사용 가능한 API의 예시입니다. 브라우저에서 실제 성공하는지는 대상 서버의 CORS 응답 설정에 따라 달라집니다. JavaScript에 Access-Control-Allow-Origin 요청 헤더를 임의로 넣는다고 해결되지 않습니다. 이 헤더는 응답 서버가 설정합니다.
8. ASP.NET Core에서 CORS 설정하기
ASP.NET Core의 Program.cs에서 허용할 프런트엔드 출처를 명시적으로 등록할 수 있습니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("Frontend", policy =>
policy.WithOrigins("https://csharp.ihavenomoney.co.kr")
.WithMethods("GET", "POST")
.WithHeaders("Content-Type", "Authorization"));
});
var app = builder.Build();
app.UseRouting();
app.UseCors("Frontend");
app.UseAuthorization();
app.MapGet("/api/hello", () => new { message = "Hello" });
app.Run();
이 예제는 최소 API를 가정합니다. 실제 프로젝트에서는 인증·인가 및 엔드포인트 설정에 맞게 미들웨어 순서를 조정해야 합니다. 쿠키 인증이 필요하다면 신뢰할 수 있는 특정 오리진만 지정하고 AllowCredentials()를 검토하며, 클라이언트에도 credentials: 'include'가 필요할 수 있습니다. 쿠키의 SameSite·Secure 속성도 별도로 검토해야 합니다.
9. WordPress REST API와 Origin
WordPress REST API는 보통 /wp-json/wp/v2/ 경로를 사용합니다. 다른 하위 도메인에서 WordPress API를 읽으려면 API 서버의 CORS 정책을 확인해야 합니다. 공개 글 조회와 로그인된 사용자의 글 수정은 보안 요구사항이 다릅니다.
- 공개 글 조회: CORS 응답과 공개 접근 가능 여부 확인
- 글 작성·수정: WordPress 인증과 해당 사용자의 권한 확인
- 쿠키 기반 인증: WordPress nonce, 쿠키 정책, CSRF 보호 확인
- 서버 간 요청: 브라우저 CORS 제한과 별개이며 API 인증은 여전히 필요
WordPress 사이트의 CORS를 무조건 *로 열거나 모든 출처에 인증 요청을 허용하는 설정은 피해야 합니다.
10. 자주 발생하는 CORS 오류 해결
| 증상 | 확인할 사항 |
|---|---|
| No ‘Access-Control-Allow-Origin’ header | API 서버의 응답 헤더와 허용 출처 |
| Preflight request doesn’t pass | OPTIONS 처리, 허용 메서드·헤더, 리다이렉트 |
| Credential + wildcard 오류 | * 대신 특정 Origin 및 자격 증명 설정 |
| HTTP에서 HTTPS 호출 문제 | 스킴 차이와 혼합 콘텐츠 정책 확인 |
| 401 또는 403 응답 | CORS뿐 아니라 인증·인가 문제 확인 |
Chrome·Edge 개발자 도구의 Network 탭에서 실제 요청, OPTIONS 요청, 응답 헤더, 상태 코드를 먼저 확인하세요. 브라우저에서 CORS를 비활성화하는 방식은 운영 환경의 해결책이 아닙니다.
11. Origin과 Site는 다르다
Origin은 스킴·호스트·포트 기준이고, SameSite 쿠키 정책에서 사용하는 ‘site’는 일반적으로 스킴과 등록 가능한 도메인 기준입니다. 따라서 https://a.example.com과 https://b.example.com은 서로 다른 Origin이지만 동일한 site로 취급될 수 있습니다. SameSite 쿠키 설정과 CORS 허용 여부는 서로 다른 문제입니다.
이 경우 프론트엔드에서 API를 호출하면 다른 오리진이라 브라우저가 막습니다. 이때 서버가 “이 오리진은 내가 허용한다”고 알려 주는 규칙이 CORS(Cross-Origin Resource Sharing) 입니다.
그래서 코드의 이 부분은
csharp
policy.WithOrigins(frontPageOrigins)
“어떤 프론트엔드 주소(오리진)에서 오는 요청을 허용할지”를 적는 목록입니다. 설정에 넣을 때는 경로 없이 오리진 형태로 적어야 합니다.
json
"AllowedOrigins": [
"http://localhost:3000", // O 올바름
"https://myfront.example.com" // O 올바름
]
"AllowedHosts": "*" 란?
appsettings.json에 기본으로 들어 있는 설정이며, 요청의 Host 헤더가 어떤 값이어도 허용한다는 뜻입니다.
json
{
"AllowedHosts": "*"
}
동작 방식
ASP.NET Core에는 Host Filtering 미들웨어가 있습니다. 요청이 들어오면 HTTP Host 헤더(예: api.example.com)를 확인해서, AllowedHosts에 적힌 목록에 없으면 400 Bad Request로 거부합니다.
| 설정값 | 의미 |
|---|---|
"*" | 모든 호스트 허용 (필터링 안 함) |
"api.example.com" | 이 호스트로 온 요청만 허용 |
"api.example.com;admin.example.com" | 세미콜론으로 여러 개 지정 |
"*.example.com" | 서브도메인 와일드카드 |
CORS의 오리진과 뭐가 다른가
헷갈리기 쉬운데 서로 완전히 다른 설정입니다.
AllowedHosts | Cors:AllowedOrigins | |
|---|---|---|
| 확인 대상 | 요청의 Host 헤더 (내 서버의 주소) | 요청을 보낸 프론트엔드의 오리진 |
| 질문 | “이 서버 주소로 부른 게 맞나?” | “이 사이트에서 부르는 걸 허용할까?” |
| 차단 방식 | 서버가 400 반환 | 브라우저가 응답을 막음 |
예를 들어 프론트엔드 https://myfront.example.com이 API https://api.example.com을 호출하면:
AllowedHosts는api.example.com(서버 쪽 주소)을 봅니다.AllowedOrigins는https://myfront.example.com(호출한 쪽 주소)을 봅니다.
"*"는 안전한가?
개발 중에는 문제없고 기본값이라 대부분 그대로 둡니다. 다만 운영 환경에서는 실제 서비스 도메인으로 제한하는 것이 권장됩니다. Host 헤더를 조작한 요청(Host Header Injection)으로 비밀번호 재설정 링크 같은 절대 URL 생성이 오염되는 공격을 줄일 수 있기 때문입니다.
json
"AllowedHosts": "api.example.com"
다만 보통 앞단에 리버스 프록시나 로드밸런서가 Host를 검증해 주는 구성이라면 "*"로 두어도 실질적 위험은 작습니다. 이 프로젝트처럼 URL을 만들어 내보내는 로직이 없는 API 서버는 더욱 그렇습니다.
AllowedOrigins가 appsettings.json에 없으면?
앞서 설명드린 “설정이 없는 경우”와 같습니다. 서버는 정상 실행되고, 브라우저의 교차 출처 요청만 전부 차단됩니다.
코드 흐름
csharp
var frontPageOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
Cors:AllowedOrigins키가 없으면Get<string[]>()이null을 반환합니다.?? []때문에 빈 배열이 됩니다.WithOrigins()에 빈 배열이 들어가 “허용 오리진이 0개”인 정책이 만들어집니다.- 예외는 발생하지 않습니다.
결과
| 호출 방식 | 결과 |
|---|---|
| 서버 시작 | 정상 |
| Swagger UI (같은 오리진) | 정상 |
| Postman, curl, 서버 간 호출 | 정상 (CORS는 브라우저만 적용) |
| 다른 오리진의 프론트엔드 | 브라우저가 차단 |
브라우저 콘솔에는 이런 오류가 보입니다.
Access to fetch at 'https://localhost:5001/api/...' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
참고: 다른 곳에서 값이 들어올 수도 있습니다
appsettings.json에 없어도 ASP.NET Core는 여러 설정 소스를 합쳐서 읽기 때문에, 아래 중 하나에 있으면 정상 동작합니다.
appsettings.Development.json같은 환경별 파일- 환경 변수 (배열은 인덱스를 붙입니다)
Cors__AllowedOrigins__0=http://localhost:3000
Cors__AllowedOrigins__1=https://myfront.example.com
- User Secrets, 커맨드라인 인자
그래서 “json에 없는데 왜 되지?” 또는 “json에 넣었는데 왜 안 되지?” 같은 상황이 생기면, 환경별 파일이나 환경 변수가 덮어쓰고 있지 않은지 확인해 보시면 좋습니다.
production에서는 꼭 세팅해야 하나?
프론트엔드가 브라우저에서 이 API를 직접 호출한다면 반드시 필요합니다. 반대로 브라우저 교차 출처 호출이 전혀 없다면 필요 없습니다.
필요한 경우 / 불필요한 경우
| 구성 | AllowedOrigins 필요? |
|---|---|
| 프론트엔드(React 등)가 다른 도메인에서 API를 직접 호출 | 필요 |
프론트엔드와 API가 같은 오리진 (예: 리버스 프록시로 /api를 같은 도메인에 묶음) | 불필요 |
| 서버 간 호출, 모바일 앱, Postman 등 브라우저가 아닌 클라이언트만 사용 | 불필요 |
| 프론트엔드 서버(Next.js 서버 사이드 등)가 대신 호출 | 불필요 |
이 프로젝트는 CORS 정책 이름이 "FrontPage"이고 X-Admin-Key 헤더를 허용하고 있으니, 브라우저 프론트엔드가 직접 호출하는 구조를 염두에 둔 것으로 보입니다. 그렇다면 production에서 설정해야 합니다.
꼭 appsettings.json에 넣어야 하나?
아닙니다. 어느 설정 소스든 상관없고, production에서는 오히려 아래 방식을 많이 씁니다.
1) appsettings.Production.json
json
{
"Cors": {
"AllowedOrigins": [ "https://myfront.example.com" ]
}
}
ASPNETCORE_ENVIRONMENT=Production일 때 appsettings.json 위에 덮어씁니다. 개발용 localhost 주소는 appsettings.json/Development에 두고, 운영 도메인만 여기에 두면 분리가 깔끔합니다.
2) 환경 변수 (컨테이너, 클라우드 배포에서 흔함)
Cors__AllowedOrigins__0=https://myfront.example.com
설정 파일을 수정하지 않고 배포 환경에서 바꿀 수 있습니다.
production 설정 시 주의점
https://로 정확히 적기: 프로토콜, 도메인, 포트가 완전히 일치해야 합니다. 끝에/를 붙이거나 경로를 넣으면 매칭되지 않습니다.- 개발용 주소 제거:
http://localhost:3000이 운영에 남아 있지 않게 하세요. - 와일드카드는 피하기: 이 코드는
X-Admin-Key라는 관리자 키 헤더를 쓰므로, 아무 오리진이나 허용하는 것은 위험합니다. 지금처럼 명시적 목록만 쓰는 게 맞습니다. - 빈 값이면 조용히 실패: 앞서 설명했듯 오류 없이 시작되고 브라우저에서만 CORS 오류가 납니다. 배포 직후 프론트엔드 연동 확인이 필요합니다.
한 가지 더
코드에서 ?? []로 조용히 넘어가는 대신, production에서 값이 비어 있으면 시작 시점에 바로 실패하게 만들 수도 있습니다.
csharp
if (!builder.Environment.IsDevelopment() && frontPageOrigins.Length == 0)
throw new InvalidOperationException("Cors:AllowedOrigins must be configured in production.");
설정 누락을 배포 직후 바로 발견할 수 있어서, 운영에서 원인 모를 CORS 오류를 찾는 시간을 줄여 줍니다.
AllowedOrigins가 AllowedHosts보다 안전한가?
“더 안전하다/덜 안전하다”로 비교하기 어렵습니다. 서로 막는 대상이 다른 별개의 방어 장치이고, 둘 다 인증을 대신하지 못합니다.
각각이 막는 것
AllowedHosts | Cors:AllowedOrigins | |
|---|---|---|
| 막는 위협 | Host 헤더 조작 (Host Header Injection) | 악성 웹사이트가 사용자 브라우저를 이용해 API 응답을 읽는 것 |
| 누가 강제하나 | 서버가 요청을 거부 (400) | 브라우저가 응답을 막음 |
| 우회 가능성 | 헤더를 마음대로 바꿔 보내면 목록에 있는 값으로 위조 가능 | curl, Postman, 서버 코드는 CORS를 아예 무시 |
둘 다 “보안 장치”로 과신하면 안 되는 이유
CORS는 서버를 보호하지 않습니다. 브라우저가 “다른 사이트의 JS가 응답을 읽지 못하게” 막을 뿐이라, 공격자가 curl이나 스크립트로 직접 API를 호출하면 CORS와 무관하게 요청이 서버에 도달합니다. 이 프로젝트에서 실제로 API를 지키는 것은 CORS가 아니라 X-Admin-Key 검사 미들웨어입니다.
Host 검사도 위조가 쉽습니다. 공격자가 직접 요청을 보낼 때는 Host 헤더를 허용 목록의 값으로 적어 보내면 통과합니다. 그래서 주로 리버스 프록시 뒤에서 잘못된 Host로 URL이 생성되는 사고를 줄이는 용도입니다.
이 프로젝트에서의 우선순위
보호 효과가 큰 순서대로 보면:
- API 키 인증 미들웨어 (실제 접근 통제, 가장 중요)
AllowedOrigins(브라우저 환경에서 다른 사이트가 응답을 읽는 것을 차단, 지금처럼 명시적 목록 유지)AllowedHosts(보조적 방어,"*"여도 이 API는 URL을 생성해 내보내지 않아 실질 위험이 작음)
정리하면, 브라우저 프론트엔드를 쓰는 이 구조에서는 AllowedOrigins를 정확히 설정하는 것이 AllowedHosts보다 실무적으로 더 의미가 큽니다. 다만 “누가 이 API를 호출할 수 있는가”를 결정하는 것은 둘 다 아니고 인증이라는 점을 기억해 두세요.
둘 다 쓰면 더 안전한가?
네, 조금은 더 안전해집니다. 다만 서로 막는 대상이 달라서 겹치는 방어가 아니라 서로 다른 빈틈을 각각 메우는 것이고, 효과는 “보조적”입니다.
함께 쓰면 얻는 것
| 조합 | 효과 |
|---|---|
AllowedOrigins만 | 다른 사이트의 브라우저 JS가 응답을 읽는 것 차단 |
AllowedHosts만 | 엉뚱한 Host 헤더로 들어오는 요청 거부 |
| 둘 다 | 두 종류의 위협을 각각 줄임 (위 둘을 합친 효과) |
한쪽이 다른 쪽을 대신해 주지 않기 때문에, 둘 다 설정하면 막는 범위가 넓어집니다.
운영 환경 설정 예시
json
{
"AllowedHosts": "api.example.com",
"Cors": {
"AllowedOrigins": [ "https://myfront.example.com" ]
}
}
AllowedHosts: 내 API 서버의 도메인AllowedOrigins: 프론트엔드의 도메인
두 값이 서로 다른 주소라는 점만 주의하면 됩니다. (프론트엔드 주소를 AllowedHosts에 넣으면 정상 요청이 400으로 거부될 수 있습니다.)
한계: 이 둘이 있어도 못 막는 것
- curl, Postman, 스크립트로 직접 호출하는 공격자 (CORS 무시, Host 위조 가능)
- 유출된
X-Admin-Key를 가진 사람
그래서 실제 방어선은 여전히 API 키 인증이고, 두 설정은 그 위에 얹는 추가 방어층입니다. 보안 강화를 위해 우선순위를 두자면 아래 순서가 좋습니다.
- API 키를 안전하게 보관하고 주기적으로 교체 (더 나아가 JWT/OIDC로 전환)
AllowedOrigins를 운영 도메인으로 정확히 제한AllowedHosts를 API 도메인으로 제한- HTTPS 강제, 로드밸런서/프록시 단의 접근 제한
정리하면, 둘 다 쓰는 것이 권장되지만 “둘 다 썼으니 안전하다”가 아니라 “인증이 있고, 그 위에 보조 방어가 겹쳐 있다”로 이해하시면 정확합니다.
– 정리 –
Origin = Scheme + Host + Port. URL의 경로나 쿼리는 오리진 비교에 영향을 주지 않습니다. SOP는 다른 출처의 데이터 접근을 제한하며, CORS는 서버가 특정 교차 출처 읽기를 허용하는 메커니즘입니다. 보안상 인증, 권한 검사, CSRF 보호를 CORS와 혼동하지 않는 것이 중요합니다.