비루한(...) 프론트엔드 실력으로 HTML과 스크립트만으로 프로젝트를 진행하던 와중, Script 태그 안에다가 모든것을 박아넣어 동작을 구현하려 했다.
<script>
dosomething();
</script>
이런 식으로... 하지만 스크립트가 동작하지 않았고, 노트북에 샷건을 칠 뻔 했으나 가까스로 이성의 끈을 붙잡고 브라우저 콘솔을 봤다.
이런 에러가 나를 반겨줬다. 아니 이게 뭔 에러지?
일단 CSP가 뭔지 알아보자.
MDN Web Doc에 의하면 이렇게 되어있다.
Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft, to site defacement, to malware distribution.
해석은 다음과 같다
콘텐츠 보안 정책(CSP)는 한 층의 보호막으로서, Cross-Site Scripting(XSS)를 포함한 특정 공격을 감지하고 피해를 완화시킬 수 있습니다. 이 공격들은 데이터 갈취, 사이트 모양의 변형, 멀웨어 배포 등을 목적으로 할 수 있습니다.
즉 XSS를 주로 막는 것인데, XSS는 간단히 설명해 자바스크립트 코드 등을 웹사이트 내에 삽입시켜 (ex. 글쓰기 등으로) 공격을 하는 것이라고 보면 된다. 자세한 내용까지는 설명하지 않겠다.
좋은 보안 기능이지만... 지금은 내 스크립트의 실행까지 막아버리고 있다 ㅠㅠ 그렇다면 어떻게 해결해야 하는 것일까?
해결 방법은 위 에러 메시지에 다 나와있다.
html의 head의 meta 테그에 적용시키는 것이 직접적인 방법이지만, 나는 node.js express와 보안을 위한 helmet 패키지를 사용하여 좀 더 가독성이 좋게 해결하였다.
일단 필요한 패키지들은 다음과 같다.
npm install express
npm install helmet
npm install helmet-csp
그리고 적용 예시는 다음과 같다. 서버가 설치된 main.js 파일에 다음과 같이 적용하면 된다.
main.js
const express = require('express');
const app = express();
const helmet = require('helmet');
const csp = require('helmet-csp');
app.use(helmet());
app.use(
csp({
/*
-- CSP 내용! --
*/
},
})
);
...
...
...
const PORT = process.env.PORT || 3000;
app.listen(PORT, console.log("Server start at port : "+PORT));
그리고 -- CSP 내용! -- 이 부분에 작성해야 되는 내용을 2번 해결 방법 별로 정리하도록 하겠다.
directives: {
scriptSrc: ["'self' 'unsafe-inline'"],
},
앞에서 얘기했듯이 비추하는 내용이다.
directives: {
scriptSrc: ["'self' 'sha256-M3V8NKjeN39ziwIOAwhdbSTM5mU2IAzqz2VIWIx5sLg='"],
},
브라우저로 구글 크롬을 사용중이라면, 친절하게도 에러 메시지에서 해쉬값을 알려준다. 이 값을 적용하면 된다.
근데 가끔 버튼, select 등에 onchange등을 삽입하여 inline event를 사용하려면 안되는 경우가 있다. 이 경우엔 'unsafe-hashes' 키워드를 삽입하면 해결이 되는데, 주의할 점은 사파리에서 지원을 하지 않는 키워드라, 사파리에선 동작하지 않는다!
좀 장황해서 무슨 말인지 이해가 가지 않을텐데, https://stackoverflow.com/questions/71331730/how-to-use-html-onselect-without-violating-csp/71331791#71331791 여길 참고해보자. 나타나는 에러 메시지가 위와 다를 것이다.
directives: {
scriptSrc: ["'self' 'nonce-r@ndom"],
},
...
...
<script nonce='r@ndom'>
dosomething();
</script>
...
...
nonce는 앞에서 말했듯이 스크립트에 직접 암호를 지정하는 방식이다. 은행 OTP같다고 하달까? 다만 이 방식은 nonce값을 알아내지 못하도록 몇 가지 방법을 권장하고 있다.
비밀번호를 알아내버리면 공격이 가능해 버리니까... CSP를 만든 쪽에서는 다음과 같이 권장하고 있다.
- nonce값은 HTTP 리퀘스트마다 고유할 것 (HTTP 리퀘스트마다 값을 바꾸란 얘기다.)
- nonce는 안전한 랜덤 비밀번호 생성기를 활용할 것 (나라면 main.js에서 생성해 html로 전달하는 방법을 택할 것 같다)
- nonce 비밀번호는 충분한 길이를 가져야 하며, 적어도 128비트 엔트로피를 택할 것 (hex로 32자 혹은 base64로 24자)
- nonce에서 사용될 수 있는 글자는 base64 인코딩으로 생성될 수 있는 글자로 한정됨.
- nonce를 사용하는 스크립트 태그에선 신뢰할 수 없거나 / escape되지 않은 변수를 사용하지 말 것.
이 방법은 script 태그가 자주 바뀌는 프로젝트에 권장되는 방법인데, 나는 개인적으로 택하지 않았다... 라우터를 통해 렌더링 하다보니 값을 넘겨줄 방법이 마땅히 생각되지 않아서........ 방법은 있겠지만 솔직히 귀찮았다... ㅎㅎ...
여하튼 좀 장황했는데, 알아내는데 시간이 오래 걸렸던... 나름 대처방법이 쉬운데 한참 헤멨던 그런 오류였다 ㅠㅠ
P.S. script 태그 뿐만 아니라 img 태그 등 외부에서 불러오거나 injection이 가능한 태그에는 모두 적용되는 정책인 것 같다. 비슷한 에러 메시지가 나오더라도 당황하지 말자!
참고
1. https://content-security-policy.com/
2. https://www.npmjs.com/package/helmet-csp