이 문서는 크로스 사이트 스크립팅 공격과 같은 일반적인 웹 애플리케이션 취약점과 공격에 대응하는 Angular의 내장 보호 기능을 설명합니다. 인증이나 인가처럼 애플리케이션 수준에서 다루는 보안은 설명하지 않습니다.
아래에 설명하는 공격과 대응 방법에 대한 자세한 내용은 Open Web Application Security Project(OWASP) 가이드를 참고하세요.
취약점 신고하기
Angular는 Google 오픈소스 소프트웨어 취약점 보상 프로그램에 참여하고 있습니다. Angular의 취약점은 https://bughunters.google.com에서 신고해 주세요.
Google의 보안 문제 처리 방식에 대한 자세한 내용은 Google의 보안 철학을 참고하세요.
모범 사례
Angular 애플리케이션의 보안을 유지하기 위한 모범 사례는 다음과 같습니다.
- Angular 라이브러리를 최신 버전으로 유지하세요. Angular 라이브러리는 정기적으로 갱신되며, 이전 버전에서 발견된 보안 결함이 수정될 수 있습니다. Angular 변경 이력에서 보안 관련 갱신 사항을 확인하세요.
- 사용 중인 Angular 자체를 수정하지 마세요. Angular를 비공개로 수정한 커스텀 버전은 최신 버전보다 뒤처지기 쉬우며, 중요한 보안 수정과 개선 사항이 포함되지 않을 수 있습니다. 대신 Angular 개선 사항을 커뮤니티에 공유하고 풀 리퀘스트를 보내세요.
- 문서에 "Security Risk"로 표시된 Angular API는 사용을 피하세요. 자세한 내용은 이 페이지의 안전한 값을 신뢰하도록 지정하기 섹션을 참고하세요.
크로스 사이트 스크립팅(XSS) 방지하기
크로스 사이트 스크립팅(XSS)은 공격자가 웹 페이지에 악성 코드를 삽입할 수 있게 하는 공격입니다. 이 코드는 사용자 정보나 로그인 정보를 훔치거나 사용자를 사칭해 작업을 수행하는 데 사용될 수 있습니다. 웹에서 가장 흔한 공격 중 하나입니다.
XSS 공격을 차단하려면 악성 코드가 문서 객체 모델(DOM)에 들어가지 못하도록 해야 합니다.
예를 들어 공격자가 개발자를 속여 DOM에 <script> 태그를 삽입하게 만들면 웹사이트에서 임의의 코드를 실행할 수 있습니다.
이 공격은 <script> 태그에만 한정되지 않습니다.
<img alt="" onerror="...">나 <a href="javascript:...">처럼 DOM의 여러 엘리먼트와 프로퍼티에서 코드를 실행할 수 있습니다.
공격자가 제어하는 데이터가 DOM에 들어오면 보안 취약점이 발생할 수 있다고 보아야 합니다.
Angular의 크로스 사이트 스크립팅 보안 모델
Angular는 XSS 버그를 체계적으로 차단하기 위해 기본적으로 모든 값을 신뢰할 수 없는 값으로 취급합니다. 템플릿 바인딩이나 문자열 바인딩을 통해 DOM에 값을 삽입할 때 Angular는 신뢰할 수 없는 값을 정제(sanitize)하고 이스케이프합니다. Angular 외부에서 이미 정제되어 안전하다고 판단한 값이라면, 신뢰할 수 있는 값으로 표시해 Angular에 알려주세요.
렌더링에 사용하는 값과 달리 Angular 템플릿은 기본적으로 신뢰할 수 있다고 간주되며, 실행 가능한 코드로 취급해야 합니다. 사용자 입력과 템플릿 구문을 이어 붙여 템플릿을 만들지 마세요. 그렇게 하면 공격자가 애플리케이션에 임의의 코드를 삽입할 수 있습니다. 이러한 취약점을 방지하려면 프로덕션 배포에서 항상 기본 AOT(Ahead-Of-Time) 템플릿 컴파일러를 사용하세요.
콘텐츠 보안 정책과 Trusted Types를 사용하면 보호 계층을 추가할 수 있습니다. 이 웹 플랫폼 기능들은 XSS 문제를 방지하기에 가장 효과적인 DOM 수준에서 동작합니다. 따라서 더 저수준의 다른 API를 사용해 우회할 수 없습니다. 이러한 이유로 이 기능들을 적극적으로 활용하는 것을 권장합니다. 애플리케이션의 콘텐츠 보안 정책을 설정하고 Trusted Types 강제 적용을 활성화하세요.
정제와 보안 컨텍스트
정제 는 신뢰할 수 없는 값을 검사해 DOM에 안전하게 삽입할 수 있는 값으로 바꾸는 과정입니다. 정제 과정에서 값이 전혀 바뀌지 않는 경우도 많습니다. 정제 방식은 컨텍스트에 따라 달라집니다. 예를 들어 CSS에서는 무해한 값이 URL에서는 위험할 수 있습니다.
Angular는 다음 보안 컨텍스트를 정의합니다.
| 보안 컨텍스트 | 설명 |
|---|---|
| HTML | 값을 HTML로 해석할 때 사용합니다. 예를 들어 innerHTML에 바인딩하는 경우입니다. |
| Style | CSS를 style 프로퍼티에 바인딩할 때 사용합니다. |
| URL | <a href>와 같은 URL 프로퍼티에 사용합니다. |
| Resource URL | <script src>처럼 불러온 후 코드로 실행하는 URL입니다. |
Angular는 HTML과 URL에서 신뢰할 수 없는 값을 정제합니다. 리소스 URL은 임의의 코드를 포함하므로 정제할 수 없습니다. 개발 모드에서는 Angular가 정제 과정에서 값을 변경해야 할 때 콘솔에 경고를 출력합니다.
정제 예제
다음 템플릿은 htmlSnippet의 값을 두 가지 방식으로 바인딩합니다.
하나는 문자열 바인딩을 사용해 엘리먼트의 내용에 넣고, 다른 하나는 엘리먼트의 innerHTML 프로퍼티에 바인딩합니다.
inner-html-binding.component.html
<h3>Binding innerHTML</h3>
<p>Bound value:</p>
<p class="e2e-inner-html-interpolated">{{ htmlSnippet }}</p>
<p>Result of binding to innerHTML:</p>
<p class="e2e-inner-html-bound" [innerHTML]="htmlSnippet"></p>
문자열 바인딩으로 넣은 내용은 항상 이스케이프됩니다. HTML로 해석되지 않으므로 브라우저는 꺾쇠괄호를 엘리먼트의 텍스트 내용으로 표시합니다.
HTML로 해석하게 하려면 innerHTML과 같은 HTML 프로퍼티에 바인딩하세요.
공격자가 제어할 수 있는 값을 innerHTML에 바인딩하면 일반적으로 XSS 취약점이 생긴다는 점에 유의하세요.
예를 들어 다음과 같은 방식으로 JavaScript를 실행할 수 있습니다.
inner-html-binding.component.ts (클래스)
export class InnerHtmlBindingComponent {
// For example, a user/attacker-controlled value from a URL.
htmlSnippet = 'Template <script>alert("0wned")</script> <b>Syntax</b>';
}
Angular는 이 값을 안전하지 않다고 인식해 자동으로 정제합니다.
script 엘리먼트는 제거하지만 <b> 엘리먼트와 같은 안전한 내용은 유지합니다.
DOM API 직접 사용과 명시적 정제 호출
Trusted Types를 강제 적용하지 않는 한, 브라우저의 내장 DOM API는 보안 취약점을 자동으로 방어하지 않습니다.
예를 들어 document, ElementRef를 통해 접근하는 노드, 여러 서드 파티 API에는 안전하지 않은 메서드가 포함되어 있습니다.
마찬가지로 DOM을 조작하는 다른 라이브러리와 상호작용할 때는 Angular 문자열 바인딩과 같은 자동 정제 기능을 제공받지 못할 가능성이 높습니다.
DOM을 직접 조작하는 것은 피하고, 가능하면 Angular 템플릿을 사용하세요.
직접 조작이 불가피하다면 Angular에 내장된 정제 함수를 사용하세요.
DomSanitizer.sanitize 메서드와 적절한 SecurityContext로 신뢰할 수 없는 값을 정제하세요.
이 함수는 bypassSecurityTrust 함수로 신뢰할 수 있다고 표시한 값도 받으며, 아래에서 설명하는 것처럼 해당 값은 정제하지 않습니다.
안전한 값을 신뢰하도록 지정하기
애플리케이션에 실행 가능한 코드를 포함하거나, 특정 URL의 <iframe>을 표시하거나, 잠재적으로 위험한 URL을 구성해야 할 때가 있습니다.
이러한 상황에서 자동 정제를 막으려면 값을 검사하고, 생성 방식을 확인했으며, 안전하다는 것을 확인했다고 Angular에 알려주세요.
이 작업은 주의해서 수행해야 합니다.
악성일 수 있는 값을 신뢰하면 애플리케이션에 보안 취약점을 만들게 됩니다.
확신이 없다면 보안 전문가의 검토를 받으세요.
값을 신뢰할 수 있다고 표시하려면 DomSanitizer를 주입하고 다음 메서드 중 하나를 호출하세요.
bypassSecurityTrustHtmlbypassSecurityTrustScriptbypassSecurityTrustStylebypassSecurityTrustUrlbypassSecurityTrustResourceUrl
값의 안전성은 컨텍스트에 따라 달라지므로, 값을 사용할 목적에 맞는 컨텍스트를 선택해야 합니다.
다음 템플릿에서 javascript:alert(...) 호출을 URL에 바인딩해야 한다고 가정해 봅시다.
bypass-security.component.html (URL)
<h4>An untrusted URL:</h4>
<p><a class="e2e-dangerous-url" [href]="dangerousUrl">Click me</a></p>
<h4>A trusted URL:</h4>
<p><a class="e2e-trusted-url" [href]="trustedUrl">Click me</a></p>
일반적으로 Angular는 URL을 자동으로 정제해 위험한 코드를 비활성화하고, 개발 모드에서는 이 작업을 콘솔에 기록합니다.
이를 막으려면 bypassSecurityTrustUrl을 호출해 해당 URL 값을 신뢰할 수 있는 URL로 표시하세요.
bypass-security.component.ts (trust-url)
private sanitizer = inject(DomSanitizer);
constructor() {
// javascript: URLs are dangerous if attacker controlled.
// Angular sanitizes them in data binding, but you can
// explicitly tell Angular to trust this value:
this.dangerousUrl = 'javascript:alert("Hi there")';
this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl(this.dangerousUrl);

사용자 입력을 신뢰할 수 있는 값으로 변환해야 한다면 컴포넌트 메서드를 사용하세요.
다음 템플릿에서는 사용자가 YouTube 동영상 ID를 입력하면 해당 동영상을 <iframe>에 불러옵니다.
<iframe src> 어트리뷰트는 리소스 URL 보안 컨텍스트에 해당합니다.
신뢰할 수 없는 출처가 사용자가 의심 없이 실행할 수 있는 파일을 몰래 다운로드하게 만드는 등의 위험이 있기 때문입니다.
이를 방지하려면 컴포넌트의 메서드를 호출해 신뢰할 수 있는 동영상 URL을 구성하세요.
그러면 Angular가 <iframe src> 바인딩을 허용합니다.
bypass-security.component.html (iframe)
<h4>Resource URL:</h4>
<p>Showing: {{ dangerousVideoUrl }}</p>
<p>Trusted:</p>
<iframe
class="e2e-iframe-trusted-src"
width="640"
height="390"
[src]="videoUrl"
title="trusted video url"
></iframe>
<p>Untrusted:</p>
<iframe
class="e2e-iframe-untrusted-src"
width="640"
height="390"
[src]="dangerousVideoUrl"
title="unTrusted video url"
></iframe>
bypass-security.component.ts (trust-video-url)
updateVideoUrl(id: string) {
// Appending an ID to a YouTube URL is safe.
// Always make sure to construct SafeValue objects as
// close as possible to the input data so
// that it's easier to check if the value is safe.
this.dangerousVideoUrl = 'https://www.youtube.com/embed/' + id;
this.videoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.dangerousVideoUrl);
}
콘텐츠 보안 정책
콘텐츠 보안 정책(Content Security Policy, CSP)은 XSS를 방지하는 심층 방어 기법입니다.
CSP를 활성화하려면 웹 서버가 적절한 Content-Security-Policy HTTP 헤더를 반환하도록 설정하세요.
콘텐츠 보안 정책에 대한 자세한 내용은 Google Developers 웹사이트의 Web Fundamentals 가이드를 참고하세요.
새로 생성한 Angular 애플리케이션에 필요한 최소 정책은 다음과 같습니다.
default-src 'self'; style-src 'self' 'nonce-randomNonceGoesHere'; script-src 'self' 'nonce-randomNonceGoesHere';
Angular 애플리케이션을 제공할 때 서버는 요청마다 무작위로 생성한 nonce를 HTTP 헤더에 포함해야 합니다.
프레임워크가 <style> 엘리먼트를 렌더링할 수 있도록 이 nonce를 Angular에 제공해야 합니다.
다음 방법 중 하나로 Angular에 nonce를 설정할 수 있습니다.
- 최상위 애플리케이션 엘리먼트에
<app ngCspNonce="randomNonceGoesHere"></app>와 같이ngCspNonce어트리뷰트를 지정합니다. 응답을 구성할 때 헤더와index.html모두에 nonce를 추가할 수 있는 서버 측 템플릿 기능을 사용할 수 있다면 이 방식을 사용하세요. CSP_NONCE주입 토큰으로 nonce를 제공합니다. 런타임에 nonce에 접근할 수 있고index.html을 캐시하려는 경우 이 방식을 사용하세요.
import {CSP_NONCE} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {AppComponent} from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
{
provide: CSP_NONCE,
useValue: globalThis.myRandomNonceValue,
},
],
});
고유한 nonce
제공하는 nonce가 항상 요청마다 고유하고, 예측하거나 추측할 수 없는 값인지 확인하세요. 공격자가 앞으로 사용될 nonce를 예측할 수 있으면 CSP가 제공하는 보호를 우회할 수 있습니다.
CDN을 사용하는 경우 응답이 자주 캐시되므로 일반적으로 원본 서버에서 nonce를 생성하는 것은 권장하지 않습니다. 서버가 nonce를 생성하고 CDN이 해당 HTML 응답을 캐시하면 이후의 모든 방문자가 같은 "고유한" 값을 받게 됩니다. 그러면 공격자가 이 고정된 값을 알아내 CSP 보호를 우회할 수 있습니다.
nonce의 "일회용" 속성을 유지하려면 콘텐츠가 사용자에게 전달되기 직전에 엣지 계층(예: CDN)에서 생성하는 것이 이상적입니다.
참고: 애플리케이션의 핵심 CSS를 인라인으로 포함하려면 CSP_NONCE 토큰을 사용할 수 없습니다.
이 경우 워크스페이스 설정의 security.autoCsp 옵션을 사용하거나 최상위 애플리케이션 엘리먼트에 ngCspNonce 어트리뷰트를 설정하세요.
프로젝트에서 nonce를 생성할 수 없다면 CSP 헤더의 style-src 항목에 'unsafe-inline'을 추가해 인라인 스타일을 허용할 수 있습니다.
| 항목 | 설명 |
|---|---|
default-src 'self'; |
페이지가 필요한 모든 리소스를 동일 출처에서 불러올 수 있도록 허용합니다. |
style-src 'self' 'nonce-randomNonceGoesHere'; |
페이지가 동일 출처('self')의 전역 스타일과 Angular가 nonce-randomNonceGoesHere를 사용해 삽입한 스타일을 불러올 수 있도록 허용합니다. |
script-src 'self' 'nonce-randomNonceGoesHere'; |
페이지가 동일 출처('self')의 JavaScript와 Angular CLI가 nonce-randomNonceGoesHere를 사용해 삽입한 스크립트를 불러올 수 있도록 허용합니다. 중요 CSS 인라인 처리 또는 하위 리소스 무결성을 활성화한 경우에만 필요합니다. 하위 리소스 무결성은 동적으로 불러오는 청크를 검증하는 임포트 맵을 삽입하며, 두 기능 모두 index.html에 인라인 스크립트를 추가합니다. |
Angular 자체는 이 설정만 있으면 정상적으로 동작합니다. 프로젝트가 커지면 애플리케이션에 특화된 추가 기능을 지원하도록 CSP 설정을 확장해야 할 수 있습니다.
응답마다 nonce를 만들 수 없는 정적 호스팅
호스팅 환경이나 CDN이 SSI, ESI, 엣지 함수 등으로 캐시된 HTML의 플레이스홀더를 교체할 수 있다면, ngCspNonce에 플레이스홀더를 넣어 index.html을 빌드할 수 있습니다. 예를 들어 <app ngCspNonce="__CSP_NONCE__"></app>을 사용하고, 응답마다 고유한 nonce로 교체합니다.
엣지에서 변환하지 않고 index.html을 그대로 제공하는 정적 호스트에 배포한다면 고정된 nonce를 하드코딩하지 마세요. 대신 다음 방법 중 하나를 사용하세요.
autoCsp로 인라인 스크립트 해시 생성하기
워크스페이스 설정에서 security.autoCsp 옵션을 true로 설정하세요.
Angular CLI는 빌드 시 index.html의 모든 인라인 스크립트에 대해 해시를 계산합니다. 중요 CSS 인라인 처리와 하위 리소스 무결성 기능이 추가한 스크립트도 포함합니다.
CLI는 <script src> 엘리먼트를 해시가 있는 로더 스크립트로 교체하고, <head> 시작 부분에 다음 정책을 담은 <meta> 태그를 추가합니다.
script-src 'strict-dynamic' 'sha256-...' https: 'unsafe-inline'; object-src 'none'; base-uri 'self';
해시는 스크립트 내용에만 의존하므로 index.html은 모든 방문자에게 유효하며 캐시할 수 있습니다.
해시와 'strict-dynamic'을 지원하는 브라우저는 대체 소스인 https:와 'unsafe-inline'을 무시합니다.
autoCsp가 생성하는 정책에는 다음과 같은 제약이 있습니다.
- 스크립트에만 적용됩니다.
style-src는 별도로 설정해야 합니다. - 브라우저는
<meta>태그에 포함된frame-ancestors,report-uri,sandbox등의 일부 지시자를 무시합니다. 이러한 지시자는Content-Security-PolicyHTTP 헤더로 전달하세요. - 페이지에 여러 정책이 있으면 브라우저는 모든 정책을 적용합니다. CSP 헤더도 함께 전송한다면 해시가 있는 인라인 스크립트를 차단하지 않도록 헤더에서
script-src와default-src를 모두 생략하세요. autoCsp는 서버 사이드 렌더링과 함께 사용할 수 없습니다.
인라인 스크립트 사용하지 않기
Angular CLI는 중요 CSS 인라인 처리와 하위 리소스 무결성을 위해서만 index.html에 인라인 스크립트를 추가합니다. 하위 리소스 무결성 기능은 동적 모듈 임포트의 무결성 메타데이터를 전달하는 임포트 맵을 생성합니다.
optimization.styles.inlineCritical을 false로 설정하고 subresourceIntegrity를 비활성화한 상태로 유지하면 index.html에 인라인 스크립트가 포함되지 않습니다. 이때 script-src 'self'를 사용하는 Content-Security-Policy 헤더를 설정할 수 있습니다.
참고: 중요 CSS 인라인 처리를 비활성화하면 애플리케이션의 초기 렌더링이 느려질 수 있으며, 하위 리소스 무결성을 비활성화하면 스크립트 무결성 검사가 제거됩니다.
정적 호스팅에 맞게 스타일 설정하기
Angular는 실행 중에 컴포넌트 스타일용 <style> 엘리먼트를 삽입하며, 중요 CSS 인라인 처리는 index.html에 <style> 엘리먼트를 추가합니다.
autoCsp를 사용하거나 인라인 스크립트를 비활성화해도 이 스타일에는 적용되지 않습니다. 응답별 nonce가 없다면 style-src에 'unsafe-inline'을 추가해 허용하세요.
예를 들어 인라인 스크립트를 사용하지 않는 빌드라면 호스트가 다음 헤더를 보내도록 설정하세요.
default-src 'self'; style-src 'self' 'unsafe-inline';
autoCsp를 사용한다면 HTTP 헤더에 default-src나 script-src 없이 style-src 'self' 'unsafe-inline'을 보내세요.
두 경우 모두 애플리케이션 코드와 서드 파티 라이브러리에 따라 추가 지시자가 필요할 수 있습니다.
Trusted Types 강제 적용하기
크로스 사이트 스크립팅 공격으로부터 애플리케이션을 보호하는 방법으로 Trusted Types 사용을 권장합니다. Trusted Types는 더 안전한 코딩 방식을 강제해 크로스 사이트 스크립팅 공격을 방지하는 데 도움이 되는 웹 플랫폼 기능입니다. Trusted Types를 사용하면 애플리케이션 코드의 보안 점검도 간단해질 수 있습니다.
Trusted Types
애플리케이션이 지원하는 모든 브라우저에서 Trusted Types를 사용할 수 있는 것은 아닐 수 있습니다. Trusted Types를 활성화한 애플리케이션을 이를 지원하지 않는 브라우저에서 실행해도 애플리케이션 기능은 유지됩니다. 이 경우 Angular의 DomSanitizer가 XSS로부터 애플리케이션을 보호합니다. 현재 브라우저 지원 현황은 caniuse.com/trusted-types를 참고하세요.
애플리케이션에 Trusted Types를 강제 적용하려면 웹 서버가 다음 Angular 정책 중 필요한 정책을 포함한 HTTP 헤더를 보내도록 설정해야 합니다.
| 정책 | 설명 |
|---|---|
angular |
보안 검토를 마친 Angular 내부 코드에서 사용하는 정책이며, Trusted Types가 강제 적용될 때 Angular가 동작하는 데 필요합니다. 이 정책은 모든 인라인 템플릿 값과 Angular가 정제한 내용을 안전하다고 취급합니다. |
angular#bundler |
Angular CLI 번들러가 지연 로딩 청크 파일을 생성할 때 사용하는 정책입니다. |
angular#unsafe-bypass |
Angular의 DomSanitizer에서 bypassSecurityTrustHtml처럼 보안 검사를 우회하는 메서드를 사용하는 애플리케이션에 적용하는 정책입니다. 이러한 메서드를 사용하는 모든 애플리케이션은 이 정책을 활성화해야 합니다. |
angular#unsafe-jit |
JIT(Just-In-Time) 컴파일러가 사용하는 정책입니다. 애플리케이션이 JIT 컴파일러와 직접 상호작용하거나 platform browser dynamic을 사용해 JIT 모드로 실행되는 경우 이 정책을 활성화해야 합니다. |
angular#unsafe-upgrade |
@angular/upgrade 패키지가 사용하는 정책입니다. AngularJS 하이브리드 애플리케이션이라면 이 정책을 활성화해야 합니다. |
다음 위치에서 Trusted Types용 HTTP 헤더를 설정해야 합니다.
- 프로덕션 서비스 인프라
- 로컬 개발과 엔드 투 엔드 테스트를 위한 Angular CLI(
ng serve):angular.json파일의headers프로퍼티 사용 - 단위 테스트를 위한 Karma(
ng test):karma.config.js파일의customHeaders프로퍼티 사용
다음은 Trusted Types와 Angular에 맞게 설정한 헤더의 예제입니다.
Content-Security-Policy: trusted-types angular; require-trusted-types-for 'script';
다음은 DomSanitizer에서 보안 검사를 우회하는 Angular 메서드를 사용하는 애플리케이션에 Trusted Types를 적용한 헤더의 예제입니다.
Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for
'script';
다음은 JIT를 사용하는 Angular 애플리케이션에 Trusted Types를 적용한 헤더의 예제입니다.
Content-Security-Policy: trusted-types angular angular#unsafe-jit; require-trusted-types-for
'script';
다음은 모듈 지연 로딩을 사용하는 Angular 애플리케이션에 Trusted Types를 적용한 헤더의 예제입니다.
Content-Security-Policy: trusted-types angular angular#bundler; require-trusted-types-for 'script';
커뮤니티 자료
Trusted Types 설정 문제 해결에 대한 자세한 내용은 다음 자료가 도움이 될 수 있습니다.
AOT 템플릿 컴파일러 사용하기
AOT 템플릿 컴파일러는 템플릿 인젝션이라는 유형의 취약점을 방지하고 애플리케이션 성능을 크게 개선합니다. AOT 템플릿 컴파일러는 Angular CLI 애플리케이션에서 사용하는 기본 컴파일러이며, 모든 프로덕션 배포에서 사용해야 합니다.
AOT 컴파일러 대신 사용할 수 있는 JIT 컴파일러는 런타임에 브라우저 안에서 템플릿을 실행 가능한 템플릿 코드로 컴파일합니다. Angular는 템플릿 코드를 신뢰하므로 템플릿을 동적으로 생성해 컴파일하면 Angular의 내장 보호 기능을 우회하게 됩니다. 특히 사용자 데이터가 들어 있는 템플릿이 그렇습니다. 이는 보안 측면에서 피해야 하는 패턴입니다. 안전하게 폼을 동적으로 구성하는 방법은 동적 폼 가이드를 참고하세요.
서버 측 XSS 방어
서버에서 구성한 HTML은 인젝션 공격에 취약합니다. Angular 애플리케이션에 템플릿 코드를 삽입하는 것은 실행 가능한 코드를 삽입하는 것과 같습니다. 공격자가 애플리케이션을 완전히 제어할 수 있게 됩니다. 이를 방지하려면 값을 자동으로 이스케이프하는 템플릿 언어를 사용해 서버의 XSS 취약점을 막으세요. 서버 측에서 템플릿 언어를 사용해 Angular 템플릿을 생성하지 마세요. 템플릿 인젝션 취약점을 만들 위험이 높습니다.
HTTP 수준의 취약점
Angular에는 흔한 HTTP 취약점인 크로스 사이트 요청 위조(CSRF 또는 XSRF)와 크로스 사이트 스크립트 포함(XSSI)을 방지하는 데 도움이 되는 기능이 내장되어 있습니다. 두 취약점 모두 주로 서버 측에서 대응해야 하지만, Angular는 클라이언트 측 통합을 쉽게 하는 헬퍼를 제공합니다.
크로스 사이트 요청 위조
크로스 사이트 요청 위조(CSRF 또는 XSRF)에서는 공격자가 사용자를 속여 악성 코드가 있는 다른 웹 페이지(예: evil.com)에 방문하게 만듭니다.
이 웹 페이지는 애플리케이션의 웹 서버(예: example-bank.com)로 악성 요청을 몰래 보냅니다.
사용자가 example-bank.com의 애플리케이션에 로그인한 상태라고 가정해 봅시다.
사용자가 이메일을 열고 evil.com 링크를 클릭하면 새 탭에서 해당 사이트가 열립니다.
evil.com 페이지는 즉시 example-bank.com으로 악성 요청을 보냅니다.
사용자 계좌에서 공격자 계좌로 돈을 이체하는 요청일 수 있습니다.
브라우저는 인증 쿠키를 포함한 example-bank.com 쿠키를 이 요청에 자동으로 첨부합니다.
example-bank.com 서버에 XSRF 방어가 없다면 애플리케이션의 정상 요청과 evil.com이 위조한 요청을 구분할 수 없습니다.
이를 방지하려면 애플리케이션은 사용자의 요청이 다른 사이트가 아니라 실제 애플리케이션에서 시작되었음을 확인해야 합니다. 이 공격을 막으려면 서버와 클라이언트가 협력해야 합니다.
일반적인 XSRF 방어 기법에서는 애플리케이션 서버가 무작위로 생성한 인증 토큰을 쿠키에 담아 보냅니다. 클라이언트 코드는 쿠키를 읽고 이후의 모든 요청에 이 토큰이 담긴 커스텀 요청 헤더를 추가합니다. 서버는 받은 쿠키 값과 요청 헤더 값을 비교하고, 값이 없거나 서로 다르면 요청을 거부합니다.
이 기법이 효과적인 이유는 모든 브라우저가 동일 출처 정책 을 구현하기 때문입니다.
쿠키가 설정된 웹사이트의 코드만 해당 사이트의 쿠키를 읽고 해당 사이트로 보내는 요청에 커스텀 헤더를 설정할 수 있습니다.
즉, 해당 애플리케이션만 이 쿠키 토큰을 읽고 커스텀 헤더를 설정할 수 있습니다.
evil.com의 악성 코드는 할 수 없습니다.
HttpClient의 XSRF/CSRF 보안
HttpClient는 XSRF 공격을 방지하는 일반적인 방식을 지원합니다.
HTTP 요청을 보낼 때 인터셉터는 기본적으로 XSRF-TOKEN이라는 쿠키에서 토큰을 읽어 X-XSRF-TOKEN HTTP 헤더에 설정합니다.
해당 도메인에서 실행되는 코드만 쿠키를 읽을 수 있으므로, 백엔드는 HTTP 요청이 공격자가 아니라 클라이언트 애플리케이션에서 왔다는 것을 확인할 수 있습니다.
기본적으로 인터셉터는 상대 URL과 동일 출처 URL로 보내는 모든 변경 요청(예: POST)에 이 헤더를 첨부하지만, GET이나 HEAD 요청에는 첨부하지 않습니다.
GET 요청을 보호하지 않는 이유
CSRF 방어는 백엔드 상태를 변경할 수 있는 요청에만 필요합니다.
CSRF 공격은 본질적으로 도메인 경계를 넘으며, 웹의 동일 출처 정책은 공격 페이지가 인증된 GET 요청의 결과를 읽지 못하도록 막습니다.
이 기능을 사용하려면 서버는 페이지를 불러올 때나 첫 번째 GET 요청에서 JavaScript가 읽을 수 있는 XSRF-TOKEN이라는 세션 쿠키에 토큰을 설정해야 합니다.
이후 요청에서는 서버가 쿠키와 X-XSRF-TOKEN HTTP 헤더가 일치하는지 확인해, 해당 도메인에서 실행되는 코드만 요청을 보낼 수 있었다는 것을 확인할 수 있습니다.
토큰은 사용자마다 고유해야 하며 서버에서 검증할 수 있어야 합니다.
그래야 클라이언트가 자체적으로 토큰을 만들어내지 못합니다.
보안을 강화하려면 사이트의 인증 쿠키에 솔트를 더해 만든 다이제스트를 토큰으로 사용하세요.
여러 Angular 앱이 같은 도메인이나 하위 도메인을 공유하는 환경에서 충돌하지 않도록 애플리케이션마다 고유한 쿠키 이름을 지정하세요.
HttpClient는 XSRF 방어 체계의 클라이언트 측 기능만 지원합니다
백엔드 서비스는 페이지의 쿠키를 설정하고 적용 대상인 모든 요청에 헤더가 존재하는지 확인하도록 구성해야 합니다. 이렇게 하지 않으면 Angular의 기본 보호 기능은 효과가 없습니다.
커스텀 쿠키와 헤더 이름 설정하기
백엔드 서비스가 XSRF 토큰 쿠키나 헤더에 다른 이름을 사용한다면 withXsrfConfiguration으로 기본값을 재정의하세요.
다음과 같이 provideHttpClient 호출에 추가하세요.
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withXsrfConfiguration({
cookieName: 'CUSTOM_XSRF_TOKEN',
headerName: 'X-Custom-Xsrf-Header',
}),
),
],
};
XSRF 방어 비활성화하기
내장 XSRF 방어 방식이 애플리케이션에 맞지 않는다면 withNoXsrfProtection 기능으로 비활성화할 수 있습니다.
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient(withNoXsrfProtection())],
};
Open Web Application Security Project(OWASP)의 CSRF 자료는 크로스 사이트 요청 위조(CSRF)와 크로스 사이트 요청 위조(CSRF) 방지 치트 시트를 참고하세요. 스탠퍼드 대학교의 논문 크로스 사이트 요청 위조에 대한 강력한 방어에도 상세한 정보가 담겨 있습니다.
Dave Smith의 AngularConnect 2016 XSRF 발표도 참고하세요.
크로스 사이트 스크립트 포함(XSSI)
JSON 취약점이라고도 하는 크로스 사이트 스크립트 포함은 공격자의 웹사이트가 JSON API에서 데이터를 읽을 수 있게 합니다.
이 공격은 오래된 브라우저에서 JavaScript 내장 객체 생성자를 재정의한 다음 <script> 태그로 API URL을 포함하는 방식으로 동작합니다.
이 공격은 반환된 JSON을 JavaScript로 실행할 수 있을 때만 성공합니다.
서버는 모든 JSON 응답 앞에 접두사를 붙여 실행할 수 없게 만들어 공격을 방지할 수 있습니다.
관례적으로 잘 알려진 문자열인 ")]}',\n"을 사용합니다.
Angular의 HttpClient 라이브러리는 이 관례를 인식하고, 모든 응답에서 ")]}',\n" 문자열을 자동으로 제거한 다음 파싱을 진행합니다.
자세한 내용은 이 Google 웹 보안 블로그 글의 XSSI 섹션을 참고하세요.
서버 측 요청 위조(SSRF) 방지하기
Angular는 헤더 기반 서버 측 요청 위조(SSRF)를 방지하기 위해 요청 처리 과정에서 Host, Forwarded, X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Prefix, X-Forwarded-Port 헤더를 엄격하게 검증합니다.
검증 규칙은 다음과 같습니다.
Host,X-Forwarded-Host헤더와Forwarded헤더의host파라미터는 엄격한 허용 목록으로 검증하며, 경로 구분자를 포함할 수 없습니다.X-Forwarded-Port헤더는 숫자여야 합니다.X-Forwarded-Proto헤더와Forwarded헤더의proto파라미터는http또는https여야 합니다.X-Forwarded-Prefix헤더는/로 시작해야 하며, 단일 슬래시로 구분된 영문자·숫자·하이픈·밑줄만 포함할 수 있습니다.- 기본적으로
Forwarded헤더와 모든X-Forwarded-*헤더는 신뢰할 수 없다고 취급되어 요청에서 제거됩니다. 유지하려면trustProxyHeaders설정으로 명시적으로 허용해야 합니다.
유효하지 않은 헤더는 오류 로그를 발생시키며, 허용되지 않은 프록시 헤더는 요청에서 제거됩니다.
인식할 수 없는 호스트 이름이 있는 요청에는 400 Bad Request가 반환됩니다.
참고: 대부분의 클라우드 제공업체와 CDN 제공업체는 요청이 애플리케이션 원본 서버에 도착하기 전에 이 헤더들을 자동으로 검증합니다. 이러한 기본 필터링은 실제 공격 가능 영역을 크게 줄입니다.
허용할 호스트 설정하기
특정 호스트 이름을 허용하려면 허용 목록에 추가해야 합니다. 이는 배포된 애플리케이션이 올바르고 안전하게 동작하는 데 필수적입니다. 패턴에서 와일드카드를 지원하므로 호스트 이름을 유연하게 일치시킬 수 있습니다.
angular.json에서 allowedHosts 옵션을 설정할 수 있습니다.
{
// ...
"projects": {
"your-project-name": {
// ...
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"security": {
"allowedHosts": [
"example.com",
"*.example.com" // example.com의 모든 하위 도메인을 허용합니다.
]
}
// ... 다른 옵션
}
}
}
}
}
}
애플리케이션 엔진을 초기화할 때도 allowedHosts를 설정할 수 있습니다.
const appEngine = new AngularAppEngine({
allowedHosts: ['example.com', '*.trusted-example.com'],
});
const nodeAppEngine = new AngularNodeAppEngine({
allowedHosts: ['example.com', '*.trusted-example.com'],
});
Node.js용 AngularNodeAppEngine에서는 NG_ALLOWED_HOSTS 환경 변수(쉼표로 구분한 목록)로 호스트를 허용할 수도 있습니다.
export NG_ALLOWED_HOSTS="example.com,*.trusted-example.com"
중요: allowedHosts 값으로 *를 사용하면 모든 호스트 이름을 허용할 수 있지만, 일반적으로 권장하지 않으며 보안 위험이 있습니다.
임의의 Host 헤더를 허용하면 애플리케이션이 Host 헤더 인젝션과 서버 측 요청 위조(SSRF) 공격에 노출될 수 있습니다.
이 설정은 로드 밸런서나 리버스 프록시 등 다른 계층에서 Host와 X-Forwarded-Host 헤더를 검증하는 경우에만 사용해야 합니다.
보안을 강화하려면 가능한 한 허용할 호스트 목록을 명시하는 것을 권장합니다.
자세한 내용은 GHSA-x288-3778-4hhx를 참고하세요.
신뢰할 프록시 헤더 설정하기
Angular는 기본적으로 표준 Forwarded 헤더와 모든 X-Forwarded-* 헤더를 무시합니다.
애플리케이션 앞에 이 헤더들을 설정하는 신뢰할 수 있는 리버스 프록시(예: 로드 밸런서)가 있다면, Angular가 해당 헤더를 신뢰하도록 설정할 수 있습니다.
Forwarded 헤더를 신뢰하면 이 헤더에서 추출한 host와 proto 파라미터가 각각 x-forwarded-host, x-forwarded-proto 헤더보다 우선합니다.
애플리케이션 엔진을 초기화할 때 trustProxyHeaders를 설정할 수 있습니다.
const appEngine = new AngularAppEngine({
trustProxyHeaders: ['forwarded'], // 표준 Forwarded 헤더를 신뢰합니다.
});
const appEngine = new AngularAppEngine({
trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'], // 비표준 헤더를 신뢰합니다.
});
const nodeAppEngine = new AngularNodeAppEngine({
trustProxyHeaders: true, // 표준 Forwarded와 모든 X-Forwarded-* 헤더를 신뢰합니다.
});
Node.js용 AngularNodeAppEngine에서는 NG_TRUST_PROXY_HEADERS 환경 변수에 쉼표로 구분한 헤더 목록을 지정해 해당 헤더의 사용을 허용할 수도 있습니다.
export NG_TRUST_PROXY_HEADERS="X-FORWARDED-HOST,X-FORWARDED-PREFIX"
중요: 애플리케이션 앞의 신뢰할 수 있는 프록시가 이 헤더들을 엄격하게 검증하거나 덮어쓰는 경우에만 trustProxyHeaders를 활성화하세요.
그렇지 않으면 공격자가 헤더를 위조해 서버 측 요청 위조(SSRF) 공격을 일으킬 수 있습니다.
Angular 애플리케이션 보안 점검하기
Angular 애플리케이션은 일반적인 웹 애플리케이션과 같은 보안 원칙을 따라야 하며, 같은 기준으로 보안 점검을 받아야 합니다. bypassSecurityTrust 메서드처럼 보안 검토에서 확인해야 하는 Angular 전용 API는 문서에 보안상 민감한 API로 표시되어 있습니다.