돈을 받고 일을 하고 있던 그렇지 않던지 간에 어쨌든 개발을 시작하게 되면 누구나 웹 어플리케이션을 만들게 되고 한 명 이상의 사용자가 생긴다. 어플리케이션을 베포하게되면 웹 보안에 대해서 자연스럽게 관심이 가게된다.

나는 위지윅 텍스트 에디터를 커스텀으로 만들어 적용시키려다가 XSS를 알게 되고 토스트 UI 에디터를 뜯어보면서 원대한 꿈을 잠시 접어두게 되었다. XSS 공격에 노출된 텍스트 에디터도 많아서 무엇을 적용시키지 라는 고민으로 시작되어 여기까지 오게 되었다. 물론 공부를 하면서 '내가 지금 뭘하고 있는거지...?'라는 생각에 한, 두 번 빠진게 아니다.

물론 '내가 만든 어플리케이션을 누가 해킹하겠어.'라는 끔찍한 생각을 할 수도 있다. 하지만 '프론트 앤드 개발자에게 웹 보안 공부가 필요한가?'라는 질문을 하고 있다면 나의 대답은 예스이고 정말 기초적인 수준의 보안 개념에 대해서는 알고 있어야 한다고 생각한다. 프론트 앤드 개발자이면서 보안 공부를 시작하는 사람들을 위한 가이드이고 그런 분들에게 도움이 되기를 바란다.

읽기 전에 알려드립니다.

  1. Front-End-Checklist : thedaviddias, 깃헙에서 제공하는 보안 항목을 기준으로 공부했다.
  2. 웹 보안에 대해 공부하기 전에 HTTP가 무엇인지 알고 있어야 한다. 그래야 편하다. MDN의 HTTP 자습서도 훌륭하다. HTTP 완벽 가이드를 사서 보는 것도 좋다. 하지만 나는 둘다 보는걸 추천한다.(한 가지만 보다보면 이게 뭐지 하는게 많다.)
  3. 백앤드 개발 환경은 NodeJS다. 자바스크립트 외의 다른 환경의 개발자들에게 예제 코드가 도움이 되지 않을 수 있다.
  4. HTTPS와 HSTS, XSS, CSP, CSRF, CORS에 대해서 다루고 있다.
  5. 참고로 난 취준생이기 때문에 최대한 개념과 실재 적용 방법을 사례로 찾아 정리했다. 불안하면 참고하지 않아도 된다. 내가 생각하는 기준으로 '프론트 앤드 개발자라면 이정도는 알아야하지 않을까?'라고 생각해서 쓴 글이기 때문에 과할 수도 있고 부족할 수도 있다.

HTTPS - Hyper Text Transfer Secure

HTTPS는 웹 보안의 아주 기본 사항이라고 한다. 구글, 네이버를 포함해서 대부분의 사이트가 HTTPS를 적용하고 있다. 그래서 도메인을 구매할 때, SSL인증 키를 함께 사서 내가 개발한 웹 페이지에 HTTPS를 적용시켰다.

대칭키와 비대칭키

HTTP는 사람이 읽을 수 있는 언어로 되어있기 때문에 클라이언트와 서버간의 요청과 응답을 주고 받을 때, 누군가가 중간에서 가로챌수 있다. HTTPS는 그 정보를 누구나 다 볼 수 없도록 가려주는 역할을 한다.

HTTPS는 HTTP 메시지를 가려주는 방법으로 비대칭키(공개키)를 사용한다. 공개키는 서비스를 제공하는 쪽(서버)에서 사용자에게 배포하는 키다. 비공개키(개인키)는 서버에서만 가지고 있는다. 같은 공개키로 암호문을 풀 수 없고 오직 비공개키로만 풀 수 있다. 반대로 공개키로 풀 수 있는 건 한 쌍이되는 개인키로 암호화된 정보만 가능하다.

HTTPS를 적용하면 모든 내용을 암호화하는 것은 아니다. 내용은 공개키로 암호화를 하고 이 공개키를 비대칭키로 암호화를 한다.

HTTPS가 신뢰할 수 있는지 것인지 어떻게 알 수 있을까?

HTTPS는 CA 인증을 받은 회사에서 SSL 인증을 발급해준다. 브라우저 내부에는 CA 인증을 받은 회사 목록이 저장되어있다. 그래서 서버에서 클라이언트로 실어보낸 인증서는 클라이언트는 브라우저에 저장된 CA의 공개키로 복호화를 할 수 있다. 그래서 공인 인증 목록에 없는 인증서를 보내는 웹 페이지를 거를 수 있게 된다.

개인정보에 민감한 곳일 수록 SSL 인증은 아주 기본적인 사항이면서 필수 사항이라고 볼 수 있다. SSL 인증은 도메인 주소를 판매하는 곳에서 함께 구매할 수 있으며 그외에 SSL 인증을 해주는 CA에서 구매하여 나의 도메인에 적용할 수 있다.

참고한 글 네트워크 - HTTP/HTTPS 차이점, HTTPS란? : 코딩스타트
How is HTTPS different from HTTP? : SSL.com
HTTPS란? (동작방식, 장단점) : EunJeong Kwak
HTTPS가 뭐고 왜 쓰나요? (Feat. 대칭키 vs. 비대칭키) : 얄팍한 코딩사전

HTTPS로 강제 접속하게 하기

처음에 HTTPS를 구매해서 인증서를 적용하면 사용자가 도메인을 입력 했을 때, 자동으로 HTTPS로 접속하는 줄 알았다. 그런데 그렇게 되지 않았다. 찾아보니까 사용자가 HTTPS를 통해 도메인으로 접근 할 수 있도록 설정을 해주어야한다. 나는 heroku를 이용 중이고 expressjs를 사용하고 있다.

히로쿠를 통해 배포 중이라면 아래 코드는 도움이 되지 않는다.

nodejs -- http -> https 변환 ( redirect ) :취미로 하는 프로그래밍 !!!

1// 이 예제는 히로쿠에서 리다이렉션 무한 루프를 돌게된다.
2app.use((req, res, next) => {
3 if (process.env.NODE_ENV === "production" && !req.secure) {
4 res.redirect(`https:${req.headers.host}${req.url}`);
5 } else {
6 next();
7 }
8});
9
10// 이 예제는 hsts 설정이 무의미하다고 경고 메시지가 뜨게 된다.
11app.use((req, res, next) => {
12 if (process.env.NODE_ENV === "production") {
13 return req.headers["X-Forwarded-Proto"] === "http"
14 ? res.redirect(`https://${req.headers.host}${req.url}`)
15 : next();
16 } else {
17 next();
18 }
19});

위의 코드를 사용하면 히로쿠에서는 무한 루푸로 리다이렉션을 요청하다가 서버에서 에러를 뱉어내는데 이유는 아래와 같다.

Heroku, nodejitsu and other hosters often use reverse proxies which offer SSL endpoints but then forward unencrypted HTTP traffic to the website. This makes it difficult to detect if the original request was indeed via HTTPS. 출처 : express-sslify

그래서 반드시 headers에 'X-Forwarded-Proto'설정이 http인지 아닌지를 통해서 리다이렉션을 진행해주어야한다. 아래 코드를 사용하면 리다이렉션에 성공할 수 있다.

1app.use((req, res, next) => {
2 if (req.get("X-Forwarded-Proto") == "https" || req.hostname == "localhost") {
3 //Serve Angular App by passing control to the next middleware
4 next();
5 } else if (
6 req.get("X-Forwarded-Proto") != "https" &&
7 req.get("X-Forwarded-Port") != "443"
8 ) {
9 //Redirect if not HTTP with original request URL
10 res.redirect(`https://${req.hostname}${req.url}`);
11 }
12});

코드 출처 스택 오버플로우 Express.js force HTTPS/SSL redirect error: Too many redirects

hsts

마지막 단계가 남았다. helmet을 사용하고 있다면 helmet.hsts()를 설정을 해주어야한다. helmet을 사용하지 않아도 hsts를 설정할 수 있다. hsts는 옵션이기 때문에 설정하지 않아도 되지만 설정하게 되면 다음과 같은 효과를 누릴수 있다.

The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) lets a web site tell browsers that it should only be accessed using HTTPS, instead of using HTTP. Strict-Transport-Security - MDN

hsts가 설정되면 브라우저는 반드시 HTTPS로 접속해야한다고 알려주게 된다. 그래서 redirect로 https로 접속하도록 하면 사용자가 사용하는 컴퓨터에서 http를 사용해 웹 페이지에 접근하더라도 브라우저는 https만 사용한다. hstspreload.org에서 hsts가 올바르게 적용되었는지 확인할 수 있다.

성공하면 기분 좋은 녹색을 볼 수 있다.

SOP Same Origin Policy

CSP, CSRF, CORS등의 개념을 이해하기 위한 조상님 개념

CSP로 넘어가기 전에 동일 출처 정책을 알아야한다.

그 전에 출처란 무엇일까? 출처는 프로토콜, 호스트, 포트로 정의된다. 그러니까 좁은 범위에서 출처는 곧 URL이다. 예를 들어 http는 프로토콜, domain.com은 호스트, 그 뒤에 붙는 :80은 포트다.(포트는 생략되는 경우가 많다.) 이 세가지가 일치하면 동일 출처라고 부를 수 있다.

URL

http://example.com과 동일 출처로 볼 수 있는 출처는 http://example.com/api/write-document다. 다른 출처로 보는 것은 https://example.com(프로토콜이 다른 경우)이나 http://example2.com(호스트가 다른 경우) 등이 있다.

동일 출처 정책은 이렇게 A라는 출처에서 B라는 출처의 리소스가 동작하는 것을 차단하는 것이다. 그래서 외부 공격으로부터 나의 웹 어플리케이션이 안전 할 수 있다.

하지만 XSS 공격으로부터 자유로울 수 없었다. 아마도 출처 상속 때문에 XSS 공격이 가능해진것 같다.

참고 MDN : 동일 출처 정책

XSS - Cross-Site Scripting

XSS 취약점은 게시판을 구현하면서 알게 되었다. 처음에는 게시판을 커스텀하려고 시도했는데, XSS를 걸러내는 게시판을 구현하기 어렵다는 생각이 들었다. 그래서 XSS 취약점을 최소한으로 방어하고 있는 패키지를 찾아 사용하는 쪽으로 방향을 전환하게 되었다. XSS 공격에 대비하지 않았을 경우 사용자에게 끼칠 수 있는 악영향이 매우 크다.

웹 해킹 강좌 ⑦ - XSS(Cross Site Scripting) 공격의 개요와 실습 (Web Hacking Tutorial #07) : 동빈나

해커가 스크립트를 저장하거나 반사해서 웹 페이지의 개인정보를 탈취하 등의 일을 할 수 있다. 시연 영상을 보면 신기하면서도 아찔하다. XSS를 시도하는 공격은 보안 전문가가 방어를 잘 했다고 판단해도 해커가 가능한 모든 경우의 수를 찾아내려고 노력하기 때문에 미처 생각치 못한 부분에서 공격이 이루어질 수 있다. OWASP에서는 Cheat Sheet를 통해서 XSS 공격을 막을 방법에 대해서 소개하고 있다.

Dompurify나 HTML Sanitizer와 같은 패키지는 <, >, ", ', javascript:, sciprt, input 등의 문자를 걸러서 애초에 쓰기 단계에서부터 공격 가능성을 제외하도록 하게 한다.

helmet.xssFilter() - 조금 다른 이야기

조금 다른 이야기지만 helmet.xssFilter() 설정에 대한 이야기를 잠깐 해볼까 한다.

ExpressJS에서 프러덕션 우수사례:보안에서 helmet을 사용할 것을 권장하고 있다. helmet이 만능은 아니지만 최소한의 조치라고 생각된다.

helmet에 관련된 아티클 What is Helmet.js & Why it is a Security Best Practice For Express.js

1// default X-XSS-Protection : 0
2app.use(helmet.xssFilter());

helmetjs/helmet helmetjs 깃헙 사이트에서 xssFilter 옵션 사용 방법을 알 수 있다. helmet 이슈 #230에서 X-XSS-Protection 헤더를 기본적으로 비활성화 해야한다고 한다. X-XSS-Protection: header should be disabled by default #230 이슈에서 X-XSS-Protection을 비활성화를 하라고 권장하는 이유는 X-XSS-Protection block 설정하는 것이 효과 없음이 발견됐기 때문인 것 같다. 크롬은 XSS Auditor를 삭제했고, 다른 많은 브라우저도 지원하지 않는다.

Update: Cross_Site_Scripting_Prevention_Cheat_Sheet #376

'아니 그럼 도데체 어떻게 막으라고?'라는 생각을 하다가 위에 이슈에서 공유해준 아티클을 읽으면서 그 궁금증을 해소할 수 있었다. 아래는 아티클의 원문을 그대로 인용했다.

Microsoft confirmed it is removing XSS Filter in Edge. “We are retiring the XSS Filter in Microsoft Edge beginning in today’s [Windows 10 Insider Preview] build,” the company said. “Our customers remain protected thanks to modern standards like Content Security Policy, which provide more powerful, performant, and secure mechanisms to protect against content injection attacks, with high compatibility across modern browsers.” 출처 : XSS protection disappears from Microsoft Edge : James Walker, the daily-swig

결론적으로 CSP로 연결된다.

CSP - Content Security Policy

이 개요는 최신 브라우저에서 XSS 공격의 위험과 영향을 현저히 줄일 수 있는 방어책인 콘텐츠 보안 정책(CSP)에 중점을 두고 있습니다. 콘텐츠 보안 정책 : 구글

SOP는 www.example.com에서는 www.example.com의 소스만 읽을 수 있고 www.attack.com의 소스를 사용할 수 없도록 한다. 동일 출처 정책을 하는 이유는 외부의 신뢰성이 불분명한 소스를 사용하지 않도록 하고 외부 공격을 막기 위해서다. 하지만 XSS 공격으로 실재로 SOP가 제대로 작동하지 않을 수 있다.

참고 SOP, CORS, CSP 개념과 우회방법(Concept & Bypass) : Dyrandy

웹 보안은 모델은 동일 출처 정책에 근간을 두고 있다. SOP(동일 출처 정책)는 이론적으로 완벽했으나 XSS 공격으로 시스템이 파괴되었다고 한다. 그래서 CSP를 통해 XSS 공격을 최소화 할 수 있다고 소개한다. CSP는 서버가 클라이언트에게 허용되는 소스 목록을 제공하게 한다. CSP에서 설정되지 않은 소스는 바로 오류를 발생시키고 사용할 수 없게 막는다.

CSP 옵션 다양하다. helmetJS에서 기본값으로 설정해서 사용할 수 있지만 웬만하면 어떤 리소스를 허용할 것인지 옵션값을 넣는 것이 좋다. 내가 생각했을 때, CSP 옵션 설정은 사전 설계를 어느정도까지 치밀하게 했느냐에 따라서 여러 옵션을 다양하게 설정할 수 있는 것 같다. 이것도 내가 개발하는 어플리케이션의 규모에 따라서 달라질 수 있다

참고로 helmet CSP를 기본값으로 설정하면 Webpack develop mode로 build한 스크립트가 eval 함수 때문에 막히게 된다. 그래서 개발하는 중에는 'unsafe-eval'을 허용해서 스크립트가 막힘이 없게 하고 production 레벨에서 'self'로 바꿀 수 있다.

CSP 평가는 CSP EVALUATION에서 할 수 있다.

CSRF - Cross Site Request Forgery

교차 사이트 요청 위조에 대한 자료를 읽어보면 무섭다. 인증된 나의 권한으로 공격자가 의도한 기능을 실행시켜 웹 어플리케이션의 개인 정보를 유출하는 등의 일을 할 수 있기 때문이다. 특정 웹 사이트가 사용자의 웹 브라우저를 신용하는 것을 이용해서 공격하는 방법이기 때문에 로그인이 되어있는 상태에서 공격자의 스크립트가 담긴 게시물을 열어보는 행위를 하게 되면 나도 모르는 사이에 공격자가 의도한 행동을 하게 된다. 비밀번호를 바꾼다던지, 게시물을 올린다던지, 어떤 물건을 구매하게 할수도 있으며 서비스를 중지 시키는 등의 일도 가능할 수 있다고 한다.

expressjs에서 csurf란 패키지를 설치하고, 예제에 나와있는데로 csrf를 설정해서 form에 token을 불러와 인증하도록 하게 한다.미들웨어에서 csurf token을 확인하고 token 값이 있으면 게시물을 등록하게 한다. 만약 token값이 일치하지 않으면 게시물을 작성하지 못한다.

CSRF Tokens의 원리는 다음과 같다.

  1. 서버에서 클라이언트에게 토큰을 보낸다.
  2. 클라이언트는 form과 함께 토큰을 제출한다.
  3. token이 다르면 서버는 요청을 거부한다.

만약 공격자가 CSRF 토큰을 가지려면 반드시 자바스크립트를 사용해야한다. 그러나 CORS가 다른 출처를 지원하지 않으면 탈취가 불가능하다.

Understanding CSRF

참고 CSRF protection in ExpressJS

1// middleware
2import csurf from "csurf";
3export const csrfProtect = csurf({ cookie: true });
4export const local = (req, res) => {
5 res.local.csrfToken = req.csrfToken();
6};
7
8//app
9// body-parser와 express-session 아래
10app.use(csrfProtect);
1// 나는 pug를 사용했지만 html이나 기타 라이브러리도 값을 전달하기만하면 된다.
2
3// ajax
4meta(name="csrf-token", content="csrfToken")
5<!-- or -->
6// inline
7form
8 input(type="hidden", name="_csrf", value="csrfToken")

참고
CSRF란?
웹 해킹 강좌 ⑩ - CSRF(Cross Site Request Forgery) 공격 기법 (Web Hacking Tutorial #10)
csurf

CORS - Cross-Origin Resource Sharing

교차 출처 리소스 공유는 도메인 A에서 도메인 B에게 XMLHttpRequest를 사용하여 리소스를 요청하는 경우에 해당한다. CORS는 CSP와 비슷해보였다. CORS는 다른 출처에서 불러온 소스를 HTTP 요청을 할때 내가 요청하는 HTTP가 안전한 출처라고 서버에 미리 알려주는 것이다. 보통 자바스크립트에서 fetch를 통해 외부 리소스를 요청할 때, 오류가 발생할 수 있는데 SOP(Same Origin Policy)때문에 막힐 수 있다.

SOP의 존재는 나를 매우 불편하게 만든다. 하지만 SOP가 존재하는 이유는 인터넷 상에 존재하는 모든 소스를 신뢰할 수 없기 때문이다. 그 소스가 해커에 의해서 만들어진 소스일 수도 있다. XSS나 CSRF 공격을 막기 위한 조치이기도 하다. expressjs 환경에서는 cors 패키지를 사용하여 CORS 환경을 세팅하게 한다.

나는 font-face 때문에 cors를 설정했을 때 CSP 에러를 만났다. 무엇이 원인일까... CORS 옵션을 전부 지우고 style-src를 설정해주면 외부 글꼴이나 font-awsome CDN은 불러와서 읽는다. 하지만 CORS를 설정하게 되면 font가 막히면서 어플리케이션이 작동을 안한다.

찾아보면서 이 해결책이 맞는지는 모르겠지만 @font-face에 대해서 리스폰스 헤더 값을 와일드 카드로 설정하도록 권장하고 있었다. 하지만 이렇게 설정하게 되면 CORS가 무슨 소용이지 싶다. Access-Control-Allow-Origin을 전부 허용하는 건데, 특정 접근에만 와일드 카드를 적용하는 것인지 아닌지를 모르겠다. (스텍 오버 플로우에 질문을 남겼는데 해결할 수 있을까?)

참고 CORS는 왜 이렇게 우리를 힘들게 하는걸까? : Evans Libary
@font-face속성에서 url()로 폰트 리소스 호출 시 만난 CORS에러
@font-face > Cross Domain Fonts CORS > cors : npm
SOP(Same Origin Policy)

마무리

웹 보안은 사용자 경험에서 가장 중요한 영역이 아닌가 싶다. 해당 웹 서비스를 신뢰해야만 서비스 이용률이 높아 질 수 있기 때문이다. 사용자의 개인 정보가 유출되어 사용자 피해 사례를 남기게 되면 해당 서비스에 오랫동안 주홍 글씨가 각인된다. 사용자가 이용하면서 찝찝함을 느끼면 무언가 판매하는 등에 큰 장애를 가지게 된다. 보이는 영역에서 사용자의 신뢰를 높이는 방법은 빠른 응답과 일관된 UI 등이 있다. 하지만 보이지 않는 영역의 보안이 매우 중요하다.

기본적인 웹 보안에 대해서 공부는 했지만 사실 코드를 작성하면서 이게 맞나 싶은게 너무 많았다. 패키지를 가져다가 쓰긴 했지만 옵션값 설정으로도 보안 수준이 확 달라질 수 있기 때문이다. 게다가 하나 하나의 덩어리가 생각보다 엄청나게 커서 지금 모든 것을 다 이해했다고 보기도 어렵다. 정말 대략적인 것만 이해했다고 볼 수 있다.

웹 개발을 하면서 남는건 즐거움과 두려움이다. 특히 두려움은 내가 알지 못하는 영역을 마주했을 때 생긴다. 두려움을 극복하려면 결국 계속 공부하고 다시 확인하는 것을 반복하는 수 밖에 없는 것 같다.