저자는 걸스 후 코드를 기부 대상으로 선정하여 기부를 위해 쓰기 프로그램의 일환으로 기부했습니다.
소개
웹사이트를 방문할 때 다양한 자원이로드되고 렌더링됩니다. 예를 들어 https://www.digitalocean.com
에 들어가면 브라우저가 HTML과 CSS를 직접 digitalocean.com
에서 다운로드합니다. 그러나 이미지 및 기타 자산은 assets.digitalocean.com
에서 다운로드되며 분석 스크립트는 각각의 도메인에서 로드됩니다.
일부 웹사이트는 콘텐츠를로드하고 렌더링하는 데 다양한 서비스, 스타일 및 스크립트를 사용하며 브라우저는 모두 실행합니다. 브라우저는 코드가 악의적인지 여부를 알지 못하기 때문에 사용자를 보호하는 것은 개발자의 책임입니다. 웹사이트에는 많은 리소스가 있을 수 있으므로 브라우저에 승인된 리소스만 허용하는 기능이 있는 것이 좋습니다. 이것이 콘텐츠 보안 정책 (CSP)의 목적입니다.
CSP 헤더를 사용하면 개발자는 특정 리소스가 실행되도록 명시적으로 허용하고 다른 모든 리소스를 방지할 수 있습니다. 대부분의 사이트는 100개 이상의 리소스를 가질 수 있으며 각 리소스는 특정 범주의 리소스에 대해 승인되어야 하므로 CSP를 구현하는 것은 번거로운 작업일 수 있습니다. 그러나 CSP가있는 웹사이트는 승인된 리소스 만 실행되도록 보장하기 때문에보다 안전합니다.
이 튜토리얼에서는 기본적인 Django 애플리케이션에서 CSP를 구현합니다. CSP를 사용하여 특정 도메인 및 인라인 리소스를 실행하도록 사용자 정의할 것입니다. 선택적으로 Sentry를 사용하여 위반 사항을 로깅할 수도 있습니다.
전제 조건
이 튜토리얼을 완료하려면 다음이 필요합니다:
- A working Django project (version 3 or greater is preferred), either on your local machine or a DigitalOcean Droplet. If you don’t have one, you can create one with the tutorial, How to Install Django and Set Up a Development Environment on Ubuntu 20.04.
- A web browser like Firefox or Chrome and an understanding of browser network tools. For more on using browser network tools, check out the product documentation for the Network Monitor in Firefox or the DevTools Network Tab in Chrome. For more general guidance on browser developer tools, see the guide: What are Browser Developer Tools?
- Python 3 및 Django에 대한 지식, 이를 얻을 수 있는 튜토리얼 시리즈인 Python으로 코딩하는 방법 및 Django 개발을 참고하십시오.
- CSP 위반 사항을 추적하기 위한 Sentry 계정 (선택 사항).
단계 1 — 데모 뷰 만들기
이 단계에서는 응용 프로그램이 뷰를 처리하는 방식을 수정하여 CSP 지원을 추가할 것입니다.
전제 조건으로 Django를 설치하고 샘플 프로젝트를 설정했습니다. Django의 기본 뷰는 CSP 미들웨어의 모든 기능을 따라갈 수 없을 정도로 너무 간단합니다. 따라서 이 튜토리얼에서는 간단한 HTML 페이지를 만들 것입니다.
전제 조건에서 만든 프로젝트 폴더로 이동하십시오:
django-apps
디렉토리 안에 가상 환경을 만듭니다. 이를 일반적으로 env
라고 부르지만, 프로젝트에 의미 있는 이름을 사용하세요.
이제 다음 명령을 사용하여 가상 환경을 활성화합니다:
가상 환경 내에서 views.py
파일을 프로젝트 폴더에 nano
또는 원하는 텍스트 편집기를 사용하여 만듭니다:
이제 다음을 views.py
에 추가합니다:
작업이 끝나면 파일을 저장하고 닫습니다.
새로운 templates
디렉토리에 index.html
템플릿을 만듭니다:
index.html
에 다음을 추가합니다:
우리가 만든 뷰는 이 간단한 HTML 페이지를 렌더링할 것입니다. 이 페이지에는 Hello, Sammy! 텍스트와 Sammy the Shark의 이미지가 표시됩니다.
작업이 끝나면 파일을 저장하고 닫습니다.
이 뷰에 액세스하려면 urls.py
를 업데이트해야 합니다:
views.py
파일을 가져와 하이라이트된 줄을 추가하여 새 경로를 추가합니다:
새로 만든 뷰는 이제 /
를 방문할 때 볼 수 있습니다(애플리케이션이 실행 중인 경우).
파일을 저장하고 닫습니다.
마지막으로, settings.py
에서 INSTALLED_APPS
를 업데이트하여 testsite
를 포함시켜야 합니다:
여기서는 settings.py
에 testsite
를 응용 프로그램 목록에 추가하여 Django가 프로젝트 구조에 대해 일부 가정을 할 수 있도록합니다. 이 경우, templates
폴더에 Django 템플릿이 있고 이를 사용하여 뷰를 렌더링할 수 있다고 가정합니다.
프로젝트의 루트 디렉토리(testsite
)에서 다음 명령을 사용하여 Django 개발 서버를 시작하십시오. 서버의 IP 주소로 your-server-ip
를 대체하십시오.
브라우저를 열고 your-server-ip:8000
을 방문하십시오. 페이지는 다음과 유사해야합니다:
이 시점에서 페이지에는 Sammy the Shark의 프로필 사진이 표시됩니다. 이미지 아래에는 파란색 스크립트로 Hello, Sammy! 텍스트가 표시됩니다.
Django 개발 서버를 중지하려면 CONTROL-C
를 누르십시오.
이 단계에서는 Django 프로젝트의 홈페이지로 작동하는 기본 뷰를 만들었습니다. 다음으로 응용 프로그램에 CSP 지원을 추가할 예정입니다.
단계 2 — CSP 미들웨어 설치
이 단계에서는 CSP 미들웨어를 설치하고 구현하여 뷰에서 CSP 헤더를 추가하고 CSP 기능을 사용할 수 있도록합니다. 미들웨어는 Django가 처리하는 모든 요청 또는 응답에 추가 기능을 제공합니다. 이 경우 Django-CSP 미들웨어가 Django 응답에 CSP 지원을 추가합니다.
먼저, 파이썬의 패키지 관리자인 pip
를 사용하여 Django 프로젝트에 Mozilla의 CSP 미들웨어를 설치합니다. 다음 명령을 사용하여 PyPi, 파이썬 패키지 인덱스에서 필요한 패키지를 설치합니다. 명령을 실행하려면 Django 개발 서버를 CONTROL-C
를 사용하여 중지하거나 터미널에서 새 탭을 엽니다:
다음으로, Django 프로젝트의 설정에 미들웨어를 추가합니다. settings.py
를 엽니다:
django-csp
가 설치되었으므로 이제 settings.py
에 미들웨어를 추가할 수 있습니다. 이렇게 하면 응답에 CSP 헤더가 추가됩니다.
MIDDLEWARE
구성 배열에 다음 줄을 추가하십시오:
작업이 완료되면 파일을 저장하고 닫으십시오. 이제 Django 프로젝트에서 CSP를 지원합니다. 다음 단계에서는 CSP 헤더를 추가하기 시작합니다.
단계 3 — CSP 헤더 구현
이제 프로젝트가 CSP를 지원하므로 보안을 강화할 준비가 되었습니다. 이를 위해 프로젝트를 구성하여 응답에 CSP 헤더를 추가할 것입니다. CSP 헤더는 브라우저가 특정 유형의 콘텐츠를 만났을 때 어떻게 작동해야 하는지를 알려주는 것입니다. 따라서 헤더가 특정 도메인에서만 이미지를 허용하도록 지정하면 브라우저는 해당 도메인에서만 이미지를 허용합니다.
nano 또는 선호하는 텍스트 편집기를 사용하여 settings.py
를 엽니다:
파일의 어디에서든 다음 변수를 정의하십시오:
이러한 규칙은 CSP의 보일러플레이트입니다. 이러한 라인들은 각각 이미지, 스타일시트 및 스크립트에 대해 허용된 소스를 나타냅니다. 현재 이들은 모두 문자열 'self'
을 포함하고 있으며, 이는 자신의 도메인에서만 리소스가 허용된다는 것을 의미합니다.
작업이 완료되면 파일을 저장하고 닫으십시오.
다음 명령을 사용하여 Django 프로젝트를 실행하십시오:
your-server-ip:8000
을(를) 방문하면 사이트가 손상되었음을 확인할 수 있습니다:
예상대로 이미지가 나타나지 않고 텍스트가 기본 스타일(굵은 검정색)로 나타납니다. 이는 CSP 헤더가 시행되고 페이지가 이제 더 안전해졌음을 의미합니다. 이전에 생성한 뷰가 자신의 도메인이 아닌 도메인에서 스타일시트 및 이미지를 참조하기 때문에 브라우저가 이를 차단합니다.
이제 프로젝트에 자신의 도메인이 아닌 리소스를 차단하도록 브라우저에 지시하는 작동하는 CSP가 있습니다. 다음으로, 홈페이지의 누락된 이미지와 스타일링을 수정하기 위해 CSP를 특정 리소스를 허용하도록 수정할 것입니다.
단계 4 — 외부 리소스를 허용하도록 CSP 수정하기
이제 기본 CSP가 있으므로 사이트에서 사용 중인 것을 기반으로 수정하게 될 것입니다. 예를 들어 Adobe Fonts와 임베디드 YouTube 비디오를 사용하는 웹사이트는 이러한 리소스를 허용해야 합니다. 그러나 웹사이트가 자체 도메인 내의 이미지만 표시하는 경우 이미지 설정을 제한적인 기본값으로 유지할 수 있습니다.
첫 번째 단계는 승인해야 하는 모든 리소스를 찾는 것입니다. 이를 위해 브라우저의 개발자 도구를 사용할 수 있습니다. 요소 검사에서 네트워크 모니터를 열고 페이지를 새로 고침한 다음 차단된 리소스를 살펴보십시오:
네트워크 로그에는 CSP에 의해 두 개의 리소스가 차단되었음을 보여줍니다: fonts.googleapis.com
의 스타일시트와 html.sammy-codes.com
의 이미지입니다. CSP 헤더에서 이러한 리소스를 허용하려면 settings.py
의 변수를 수정해야 합니다.
외부 도메인에서 리소스를 허용하려면 파일 유형과 일치하는 CSP의 일부에 도메인을 추가하십시오. 따라서 html.sammy-codes.com
에서 이미지를 허용하려면 CSP_STYLE_SRC
에 html.sammy-codes.com
을 추가하면 됩니다.
settings.py
를 열고 다음을 CSP_STYLE_SRC
변수에 추가하십시오:
이제 도메인에서 이미지를 허용하는 것뿐만 아니라 사이트는 이제 html.sammy-codes.com
에서 이미지도 허용합니다.
인덱스 뷰는 Google Fonts를 사용합니다. Google은 여러분의 사이트에 글꼴을 제공하며(https://fonts.gstatic.com
에서) 이를 적용할 스타일 시트도 제공합니다(https://fonts.googleapis.com
에서). 글꼴이 로드되도록 하려면 CSP에 다음을 추가하십시오:
html.sammy-codes.com
에서 이미지를 허용하는 것과 유사하게, fonts.googleapis.com
에서 스타일 시트 및 fonts.gstatic.com
에서 글꼴을 허용합니다. fonts.googleapis.com
에서 로드된 스타일 시트는 글꼴을 적용하는 데 사용됩니다. 글꼴 자체는 fonts.gstatic.com
에서 로드됩니다.
파일을 저장하고 닫으십시오.
경고: self
와 유사하게, CSP에서 unsafe-inline
, unsafe-eval
, 또는 unsafe-hashes
와 같은 다른 키워드가 있습니다. 이러한 규칙을 CSP에서 사용하지 않는 것이 매우 권장됩니다. 이러한 규칙은 구현을 쉽게 만들 수 있지만 CSP를 우회하고 무용하게 만들 수 있습니다.
더 많은 정보는 “위험한 인라인 스크립트”에 대한 Mozilla 제품 문서를 참조하십시오.
이제 Google Fonts에서 스타일 및 글꼴을 사이트에 로드할 수 있으며, html.sammy-codes.com
에서 이미지를 로드할 수 있습니다. 그러나 서버의 페이지를 방문하면 지금은 이미지만 로드되는 것을 알 수 있습니다. 그 이유는 HTML에서 사용된 인라인 스타일이 허용되지 않았기 때문입니다. 다음 단계에서 이를 수정하겠습니다.
단계 5 — 인라인 스크립트 및 스타일 사용하기
이 시점에서 CSP를 수정하여 외부 리소스를 허용했습니다. 그러나 뷰 내의 스타일 및 스크립트와 같은 인라인 리소스는 아직 허용되지 않았습니다. 이번 단계에서는 글꼴 스타일링을 적용할 수 있도록 이를 작동시킵니다.
인라인 스크립트와 스타일을 허용하는 두 가지 방법이 있습니다: 논스(nonces) 및 해시(hash). 인라인 스크립트와 스타일을 자주 수정하는 경우 CSP를 자주 변경하지 않도록 논스를 사용하십시오. 인라인 스크립트와 스타일을 드물게 업데이트하는 경우 해시를 사용하는 것이 합리적인 접근 방법입니다.
인라인 스크립트 허용을 위한 nonce
사용
먼저, 논스(nonce) 접근 방법을 사용하겠습니다. 논스는 각 요청마다 고유한 무작위 생성 토큰입니다. 사이트를 방문하는 두 사람이 있다면, 각각 승인된 인라인 스크립트와 스타일에 포함된 고유한 nonce
를 받게 됩니다. 논스를 일회용 비밀번호로 생각하면 됩니다. 이 비밀번호는 사이트의 특정 부분이 단일 세션에 대해 실행되도록 승인합니다.
프로젝트에 논스 지원을 추가하려면 settings.py
에서 CSP를 업데이트하면 됩니다. 편집할 파일을 엽니다:
settings.py
파일의 CSP_INCLUDE_NONCE_IN
에 'script-src'
를 추가합니다.
파일 어디에서나 CSP_INCLUDE_NONCE_IN
을 정의하고 'script-src'
를 추가합니다:
CSP_INCLUDE_NONCE_IN
은 nonce
속성을 추가할 수 있는 인라인 스크립트를 나타냅니다. CSP_INCLUDE_NONCE_IN
은 여러 데이터 소스가 nonce를 지원하기 때문에 배열로 처리됩니다 (예: style-src
).
파일을 저장하고 닫습니다.
인라인 스크립트에 nonce
속성을 추가하면 nonce가 생성될 수 있습니다. 이를 시도하기 위해 간단한 JavaScript 스니펫을 사용하겠습니다.
index.html
을 편집합니다:
다음 스니펫을 HTML의 <head>
에 추가합니다:
이 스니펫은 브라우저 콘솔에 Hello from the console!"
을 출력합니다. 그러나 프로젝트에는 인라인 스크립트에 nonce
가 있어야만 허용되는 CSP가 있기 때문에 이 스크립트는 실행되지 않고 오류가 발생합니다.
페이지를 새로 고침하면 브라우저 콘솔에서 이 오류를 확인할 수 있습니다:
이전 단계에서 외부 리소스를 허용했기 때문에 이미지가 로드됩니다. 예상대로 스타일은 아직 기본값입니다. 또한 예상대로 콘솔 메시지가 출력되지 않고 오류가 반환됩니다. 이를 승인하려면 nonce
를 제공해야 합니다.
이를 속성으로 추가하여 가능합니다. nonce="{{request.csp_nonce}}"
이 스크립트에
이 부분을 하이라이트된 부분과 같이 표시된 대로 편집하려면 index.html
을 엽니다:
작업이 완료되면 파일을 저장하고 닫으십시오.
페이지를 새로 고치면 이제 스크립트가 실행됩니다:Inspect Element에서 확인하면 속성에 대한 값이 없음을 알 수 있습니다:
값은 보안상의 이유로 나타나지 않습니다. 브라우저가 이미 값을 처리했습니다. DOM에 액세스 할 수있는 스크립트가 값을 액세스하여 다른 스크립트에 적용하지 못하도록 숨겨져 있습니다. 대신 페이지 소스 보기를 선택하면 브라우저가 받은 내용이 표시됩니다:
페이지를 새로 고칠 때마다 nonce
값이 변경됩니다. 이는 프로젝트의 CSP 미들웨어가 각 요청에 대해 새 nonce
을 생성하기 때문입니다.
이 nonce
값은 브라우저가 응답을 수신할 때 CSP 헤더에 추가됩니다:
브라우저가 사이트로 요청하는 모든 요청에 대해 해당 스크립트에 고유한 nonce
값이 있습니다. nonce
이 CSP 헤더에서 제공되므로 Django 서버가 해당 스크립트를 실행하도록 승인했습니다:
여러 리소스에 적용 할 수있는 nonce와 함께 프로젝트를 업데이트했습니다. 예를 들어, CSP_INCLUDE_NONCE_IN
을 업데이트하여 style-src
를 허용할 수 있습니다. 그러나 인라인 리소스를 승인하는 더 간단한 방법이 있으며, 다음에 수행 할 작업입니다.
해시 사용하여 인라인 스타일 허용하기
인라인 스크립트 및 스타일을 허용하는 또 다른 방법은 해시를 사용하는 것입니다. 해시는 특정 인라인 리소스에 대한 고유한 식별자입니다.
예를 들어, 이것은 우리 템플릿의 인라인 스타일입니다:
하지만 현재 스타일이 작동하지 않습니다. 브라우저에서 사이트를 보면 이미지가 성공적으로 로드되지만 글꼴과 스타일이 적용되지 않습니다:
브라우저의 콘솔에서 인라인 스타일이 CSP를 위반한다는 오류를 찾을 수 있습니다. (다른 오류가 있을 수 있지만 인라인 스타일에 대한 오류를 찾으십시오.)
이 오류는 스타일이 CSP에 의해 승인되지 않았기 때문에 발생합니다. 그러나 오류가 스타일 스니펫을 승인하는 데 필요한 해시를 제공합니다. 이 해시는 이 특정한 스타일 스니펫에 대해 고유합니다. 다른 스니펫은 결코 동일한 해시를 갖지 않습니다. 이 해시가 CSP 내에 배치되면 이 특정 스타일이로드 될 때마다 승인됩니다. 그러나 이 스타일을 수정하는 경우 새 해시를 가져와서 이전 해시를 새로운 것으로 교체해야 합니다.
이제 settings.py
에서 CSP_STYLE_SRC
에 해시를 추가하여 적용합니다:
CSP_STYLE_SRC
목록에 sha256-...
해시를 추가하면 브라우저가 스타일 시트를 오류없이로드할 수 있습니다.
파일을 저장하고 닫으십시오.
이제 브라우저에서 사이트를 다시로드하면 글꼴과 스타일이 성공적으로 로드됩니다:
인라인 스타일 및 스크립트가 이제 올바르게 작동합니다. 이 단계에서는 인라인 스타일 및 스크립트를 허용하기 위해 두 가지 다른 접근 방식, nonce 및 해시를 사용했습니다.
그러나 중요한 문제가 하나 있습니다. 큰 웹 사이트의 경우 특히 CSP를 유지하는 것은 까다로울 수 있습니다. CSP가 리소스를 차단할 때 언제 추적하여 해당 리소스가 악성 리소스인지 아니면 사이트의 고장난 부분인지를 판단할 수 있는 방법이 필요할 수 있습니다. 다음 단계에서는 Sentry를 사용하여 CSP가 생성한 모든 위반 사항을 로그에 기록하고 추적합니다.
단계 6 — Sentry를 사용하여 위반 사항 보고 (선택 사항)
CSP가 일반적으로 엄격하게 적용되므로 콘텐츠가 차단되는 경우를 파악하는 것이 좋습니다. 특히 콘텐츠가 차단되면 사이트의 일부 기능이 작동하지 않을 가능성이 높습니다. Sentry와 같은 도구를 사용하면 사용자에게 CSP가 요청을 차단할 때 알려줄 수 있습니다. 이 단계에서는 Sentry를 구성하여 CSP 위반 사항을 기록하고 보고합니다.
전제 조건으로 Sentry와 계정을 가입했습니다. 이제 프로젝트를 만듭니다.
Sentry 대시 보드의 왼쪽 상단에있는 프로젝트 탭을 클릭하십시오:
오른쪽 상단에있는 프로젝트 생성 버튼을 클릭하십시오:
여러 로고와 함께 제목이 “플랫폼 선택“인 것을 볼 수 있습니다. Django를 선택하십시오:
그런 다음, 아래쪽에 프로젝트 이름을 지정하십시오 (이 예제에서는 sammys-tutorial
을 사용합니다) 그리고 프로젝트 생성 버튼을 클릭하십시오:
Sentry는 settings.py
파일에 추가할 코드 스니펫을 제공할 것입니다. 이 스니펫을 나중 단계에서 추가하려면 저장하십시오.
터미널에서 Sentry SDK를 설치하십시오:
settings.py
를 다음과 같이 열어보십시오:
파일 끝에 다음을 추가하고, 반드시 대시보드에서 가져온 값으로 SENTRY_DSN
을 바꾸십시오:
이 코드는 Sentry에서 제공되며 애플리케이션에서 발생하는 모든 오류를 기록할 수 있도록 합니다. 이는 Sentry의 기본 구성이며 서버에서 이슈를 기록하기 위해 Sentry를 초기화할 필요가 있습니다. 기술적으로는 CSP 위반으로 인해 서버에서 Sentry를 초기화할 필요는 없지만, 비논스나 해시를 렌더링하는 데 문제가 있는 경우에는 이러한 오류가 Sentry에 기록됩니다.
파일을 저장하고 닫습니다.
프로젝트 대시보드로 돌아가서 기어 아이콘을 클릭하여 설정으로 이동하십시오:
보안 헤더 탭으로 이동하십시오:
report-uri
를 복사하십시오:
다음과 같이 CSP에 추가하십시오:
대시보드에서 복사한 값을 your-report-uri
로 대체하십시오.
파일을 저장하고 닫으십시오. 이제 CSP 시행으로 인해 위반이 발생하면 Sentry가 이를 해당 URI에 기록합니다. 이를 확인하려면 CSP에서 도메인 또는 해시를 제거하거나 이전에 추가한 스크립트에서 nonce
를 제거하여 시도해보십시오. 브라우저에서 페이지를 로드하면 Sentry의 Issues 페이지에 오류가 표시됩니다:
로그 수가 많아 압도된다면
settings.py
에서 CSP_REPORT_PERCENTAGE
를 정의하여 로그의 일부만 Sentry로 전송할 수도 있습니다.
이제 CSP 위반 시 알림을 받고 Sentry에서 오류를 확인할 수 있습니다.
결론
이 기사에서는 콘텐츠 보안 정책으로 Django 애플리케이션을 보호했습니다. 외부 리소스를 허용하도록 정책을 업데이트하고, 인라인 스크립트와 스타일을 허용하기 위해 nonce 및 해시를 사용했습니다. 또한 위반 사항을 Sentry로 전송하도록 구성했습니다. 다음 단계로는 CSP를 강제하는 방법에 대해 자세히 알아보기 위해 Django CSP 문서를 확인하세요.