하위 리소스 무결성, CORS, 리퍼러 정책으로 외부 하위 리소스 보호

서드 파티 CDN 또는 외부 출처에서 스크립트, 스타일시트, 글꼴 또는 이미지를 로드할 때 하위 리소스 무결성(HTML의 integrity 및 CSS url()의 integrity())을 사용하여 암호화 무결성을 확인하고, 교차 출처 사용자 인증 정보 처리 (crossorigin 및 cross-origin())를 구성하고, 발신 Referer 헤더 (referrerpolicy 및 referrer-policy())를 제한합니다. 문서 수준 콘텐츠 보안 정책 및 아웃바운드 연결 허용 목록은 웹 보안 및 연결 허용 목록으로 아웃바운드 네트워크 연결 제한을 참고하세요.

1. 변경할 수 없는 애셋의 암호화 무결성 다이제스트 생성

하위 리소스 무결성 (SRI)은 바이트가 애플리케이션에서 선언한 암호화 해시와 일치하는 경우에만 브라우저가 외부 애셋을 실행하거나 렌더링하도록 합니다.

  • 빌드 또는 출시 시점에 다이제스트를 계산하고 신뢰할 수 없는 실시간 응답에서는 계산하지 않음: 빌드 또는 출시 단계에서 알려진 양호한 로컬 애셋 바이트에서 무결성 해시를 생성하거나 (또는 패키지 작성자가 게시한 공식 다이제스트를 복사) HTML, CSS 또는 JavaScript에 하드코딩합니다. 런타임에 브라우저에서 서드 파티 URL을 가져와 자체 integrity 해시를 계산하지 마세요. 이는 원격 서버가 반환하는 모든 바이트를 신뢰하고 SRI를 무력화합니다.
  • 기본적으로 sha384- 사용: sha384- (권장 기본값), sha512- 또는 sha256-를 사용하여 무결성 메타데이터를 <algorithm>-<base64-digest>로 형식 지정합니다.
  • 외부 하위 리소스를 변경 불가능한 버전이 지정된 URL에 고정: 변경 가능한 유동 URL(latest/app.min.js) 대신 변경 불가능한 버전 고정 또는 콘텐츠 주소 지정 URL(예: https://cdn.example.com/ui@2.4.0/app.min.js)을 사용하여 항상 서드 파티 애셋을 참조하세요. 유동 URL은 사이트를 조용한 공급망 변경에 노출하고 원격 파일이 업데이트되는 즉시 SRI를 중단합니다. 공급업체 분석 태그와 같이 제자리에서 업데이트되는 변경 가능한 서드 파티 스크립트를 로드해야 하는 경우 해당 URL에서 integrity를 생략하고 대신 콘텐츠 보안 정책(strict-dynamic/nonce) 또는 iframe 샌드박스로 제한하세요.
  • 엔드포인트가 여러 유효한 페이로드를 제공할 수 있는 경우 여러 다이제스트를 공백으로 구분: SRI를 지원하는 모든 브라우저는 sha256-, sha384-, sha512-를 구현합니다. URL이 알려진 양호한 페이로드 중 하나를 합법적으로 반환할 수 있는 경우 (예: A/B 테스트, 단계적 CDN 출시 또는 콘텐츠 협상 빌드 중) 공백으로 구분된 동일한 알고리즘의 여러 다이제스트를 나열합니다 (예: integrity="sha384-variantA... sha384-variantB..."). 브라우저는 존재하는 가장 강력한 알고리즘의 나열된 다이제스트 중 하나와 일치하는 경우 응답을 수락합니다.
  • CORS 헤더를 사용하여 교차 출처 하위 리소스 제공: 브라우저는 CORS를 읽을 수 있는 경우에만 교차 출처 응답의 무결성을 확인할 수 있습니다(익명 요청의 경우 Access-Control-Allow-Origin: *, 사용자 인증 정보가 있는 요청의 경우 Access-Control-Allow-Credentials: true가 포함된 정확한 요청 출처).
# Generate a sha384- SRI hash at build time from a known-good local file
printf "sha384-%s\n" "$(openssl dgst -sha384 -binary app.min.js | openssl base64 -A)"
// Build-time / tooling helper (Node.js 19+, Deno, Bun) to compute an SRI string from known-good local bytes
export async function computeIntegrityMetadata(bytes) {
  const rawBytes =
    typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
  const digestBuffer = await crypto.subtle.digest('SHA-384', rawBytes);
  const binary = Array.from(new Uint8Array(digestBuffer), (b) =>
    String.fromCharCode(b),
  ).join('');
  return `sha384-${btoa(binary)}`;
}

2. 보안 HTML 스크립트, 스타일시트, 미리 로드

문서 수준 Referrer-Policy: strict-origin-when-cross-origin HTTP 응답 헤더를 설정하거나 (또는 <head>에 <meta name="referrer" content="strict-origin-when-cross-origin"> 포함) 기본값으로 설정하여 요청별 리퍼러 수정자를 생략하는 하위 리소스 또는 CSS url()가 교차 출처 요청에서 민감한 경로 및 쿼리 매개변수를 계속 삭제하도록 합니다.

그런 다음 외부 <script>, <link rel="stylesheet">, <link rel="preload">, <link rel="modulepreload"> 요소에서 항상 integrity, crossorigin, referrerpolicy를 함께 설정합니다.

  • integrity="sha384-...": 응답 바이트가 예상 다이제스트와 일치하지 않는 경우 리소스의 실행 또는 적용을 차단합니다 (차단된 <link rel="stylesheet">의 경우 linkElement.sheet는 null로 유지되고 error 이벤트가 실행됨).
  • crossorigin="anonymous" (공개 CDN 애셋의 권장 기본값) 또는 crossorigin="use-credentials": 교차 출처 리소스에 integrity가 설정될 때마다 필요합니다. crossorigin를 생략하면 브라우저가 no-cors 모드에서 리소스를 가져오고 (해시로 확인할 수 없는 불투명한 응답 생성) 리소스를 차단합니다.
  • referrerpolicy="no-referrer" (서드 파티 CDN의 권장 기본값) 또는 referrerpolicy="strict-origin-when-cross-origin": 문서 리퍼러 정책을 재정의하여 Referer 요청 헤더의 출처 또는 경로 정보가 서드 파티 호스트에 유출되지 않도록 합니다.
<!-- Document-level baseline referrer policy in <head> (or via Referrer-Policy HTTP header) -->
<meta name="referrer" content="strict-origin-when-cross-origin" />

<!-- External stylesheet with SRI, anonymous CORS, and no referrer -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/ui@2.4.0/styles.css"
  integrity="sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
/>

<!-- External script with SRI, anonymous CORS, and no referrer -->
<script
  src="https://cdn.example.com/ui@2.4.0/bundle.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

fetch()를 사용하여 프로그래매틱 방식으로 외부 하위 리소스를 가져올 때 일치하는 integrity, mode, credentials, referrerPolicy 옵션을 전달합니다 (다이제스트 불일치는 TypeError로 프로미스를 거부함).

const response = await fetch('https://cdn.example.com/ui@2.4.0/manifest.json', {
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
  mode: 'cors',
  credentials: 'omit',
  referrerPolicy: 'no-referrer',
});

3. url() 수정자로 CSS 이미지, 글꼴, 가져온 스타일시트 보호

CSS url() 요청 수정자(cross-origin(), referrer-policy(), integrity())는 CSS에서 직접 CSS 로드 리소스(background-image, content, mask-image, border-image-source, filter, @font-face src, @import 규칙 포함)의 가져오기 모드, 리퍼러 헤더, 하위 리소스 무결성 확인을 제어합니다.

수정자 함수 인수 문법 및 허용되는 값 목적
cross-origin(<ident>) 따옴표가 없는 키워드: anonymous (권장 기본값) 또는 use-credentials CORS를 사용하여 CSS 리소스를 가져오고 사용자 인증 정보 전송 여부를 제어합니다. 교차 출처 CSS 리소스에는 항상 integrity()와 함께 포함합니다.
referrer-policy(<ident>) 따옴표가 없는 키워드: no-referrer (서드 파티 확장 소재의 권장 기본값), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade 또는 unsafe-url 이 특정 CSS 요청의 문서 리퍼러 정책을 재정의합니다.
integrity("<string>") 따옴표 붙은 문자열: "sha384-<base64>" (또는 공백으로 구분된 다이제스트 "sha384-... sha384-...") 가져온 CSS 리소스를 렌더링하거나 적용하기 전에 예상되는 암호화 다이제스트와 비교하여 확인합니다.

url() 수정자를 작성할 때는 다음 CSS 문법 및 보안 규칙을 따르세요.

  • url("...") 내의 URL 문자열을 따옴표로 묶기: 요청 수정자는 url(...) 내의 URL이 따옴표로 묶인 <string>인 경우에만 유효합니다(따옴표로 묶이지 않은 URL은 수정자를 사용할 수 없음).
  • url()당 각 수정자 유형을 한 번만 지정: url("..." ...) 내에서 cross-origin(), referrer-policy(), integrity()를 순서에 관계없이 조합할 수 있지만 동일한 수정자 유형을 두 번 반복하는 것 (예: integrity() 수정자 두 개)은 유효하지 않습니다. 여러 해시를 제공하려면 단일 integrity("sha384-... sha384-...") 문자열 내에서 공백으로 구분합니다.
  • 항상 교차 출처 integrity()를 cross-origin(anonymous)와 페어링: @font-face src 및 mask-image는 이미 기본적으로 익명 cors 모드로 가져오지만 background-image, content, border-image-source, @import는 기본적으로 no-cors 모드로 설정되어 해시로 확인할 수 없는 불투명한 응답을 생성하고 외부 SVG filter는 기본적으로 same-origin 모드로 설정되어 교차 출처 URL을 완전히 차단합니다. cross-origin(anonymous) (또는 cross-origin(use-credentials))를 포함하면 모든 CSS 속성에서 일관된 CORS 확인이 보장됩니다.
  • @import 및 @font-face 규칙의 url("...") 내부에 수정자 배치: cross-origin(), referrer-policy(), integrity()는 url("..." ...) 내부에 유지하고 format("woff2") 및 tech(...)와 같은 글꼴 설명자는 url(...) 외부에 유지합니다.
@import url(
  "https://cdn.example.com/theme@1.2.0/tokens.css"
  cross-origin(anonymous)
  referrer-policy(no-referrer)
  integrity("sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5")
);

@font-face {
  font-family: "BrandSans";
  src: url(
    "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
  ) format("woff2");
  font-display: swap;
}

.verified-graphic {
  background-image: url(
    "https://cdn.example.com/media@1.0.0/badge.svg"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
  );
}

대체 전략

기준 타겟이 아직 CSS url() 요청 수정자 (cross-origin(), referrer-policy() 또는 integrity())를 지원하지 않는 경우 문서 수준 정책을 계단식 CSS 선언과 결합하여 하위 리소스가 계속 작동하고 보호되도록 합니다.

  1. 크로스 브라우저 SRI가 필요한 경우 CSS @import보다 HTML <link rel="stylesheet"> 선호: <link rel="stylesheet">의 subresource-integrity 및 referrer-policy는 모든 브라우저에서 널리 사용할 수 있으므로 모든 브라우저에서 무결성 확인을 시행해야 할 때마다 @import 대신 <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer">를 사용하여 외부 스타일시트를 로드합니다.
  2. 2단계의 문서 수준 Referrer-Policy 헤더 또는 <meta name="referrer"> 사용: CSS url() 내에서 referrer-policy()를 지원하지 않는 브라우저에서는 문서 수준 Referrer-Policy: strict-origin-when-cross-origin 헤더 또는 <meta name="referrer" content="strict-origin-when-cross-origin"> 요소가 교차 출처 CSS 요청에서 민감한 경로 및 쿼리 매개변수를 자동으로 제거합니다.
  3. 점진적 개선을 위해 별도의 CSS 선언을 계단식으로 적용: url("..." ...) 내의 수정자를 인식하지 못하는 브라우저는 CSS 파싱 시 전체 선언을 삭제합니다. 일반 url("...")를 먼저 선언한 다음 cross-origin() 및 referrer-policy() 선언 (이 두 수정자를 지원하지만 integrity()는 지원하지 않는 브라우저의 경우)을 선언하고 integrity("sha384-...")를 포함한 전체 선언을 선언합니다.

    • 확인되지 않은 url()를 쉼표로 구분된 동일한 @font-face src 목록에 넣지 마세요. 단일 src 설명자에 src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2");를 작성하면 런타임에 하위 리소스 무결성이 우회됩니다. CDN이 integrity() 검사에 실패한 변조된 글꼴을 제공하는 경우 브라우저는 이를 로드 실패로 처리하고 쉼표로 구분된 두 번째 url("font.woff2")로 자동 대체합니다. 별도의 src: 선언을 사용하면 지원 브라우저가 CSS 파싱 시 확인되지 않은 선언을 덮어쓰고 런타임에 변조된 글꼴을 차단합니다.
    @font-face {
      font-family: "BrandSans";
      /* 1. Parse-time fallback for browsers without url() modifier support */
      src: url("https://cdn.example.com/fonts@1.0.0/brand-sans.woff2") format("woff2");
      /* 2. Parse-time fallback for browsers supporting cross-origin() and referrer-policy() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      ) format("woff2");
      /* 3. Full protection in browsers supporting cross-origin(), referrer-policy(), and integrity() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
      ) format("woff2");
      font-display: swap;
    }
    
    .verified-graphic {
      background-image: url("https://cdn.example.com/media@1.0.0/badge.svg");
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      );
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
      );
    }
    
  4. CSS.supports()를 사용하여 JavaScript에서 CSS url() 수정자 지원 기능 감지:

export function getCssUrlModifierSupport() {
  const supportsCrossOriginAndReferrer =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer))',
    );

  const supportsUrlIntegrity =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91"))',
    );

  return { supportsCrossOriginAndReferrer, supportsUrlIntegrity };
}