에러로 인해 예상하지 못한 중단이 발생하는 일이 없도록 하자
사용자 입력을 절대로 신뢰하지 말자.
사용자는 쿼리 값을 빼먹을 수도, 여러 번 적어 배열로 보낼 수도 있다. 모든 경우에 대처가능한 코드를 작성하자.
// 예시
var arrayWrap = require("arraywrap");
//..
app.get("/search",function(req,res){
var search = arrayWrap(req.query.q || "");
var terms = search[0].split("+");
// .... 기타처리....
});
//출처: https://jeong-pro.tistory.com/68 [기본기를 쌓는 정아마추어 코딩블로그]
HTTPS를 사용하면 모든 종류의 공격에 대해 사용자를 보호하는 데 도움을 준다.
사용자에게 HTTPS 사용 적용하기
var enforceSSL = require("express-enforces-ssl");
// ....
app.enable("trust proxy"); // == app.set('trust proxy', true)
app.use(enforceSSL());
기본적으로 HTTPS를 통한 요청이 오면 나머지 미들웨어와 라우트를 계속 실행하고, HTTP요청으로 오면 HTTPS버전으로 redirection 한다.
사용자의 HTTPS 연결 유지하기
HTTPS로 접속했을 때 HTTP로 가지 않게 하기위해 HSTS(HTTP Strict Transport Security)라는 기능을 지원한다.
Strict-Transport-Security : max-age=31536000 하면 약 1년간 HTTPS로 묶어둔다.
var helmet = require("helmet");
var ms = require("ms");
//....
app.use(helmet.hsts({
maxAge: ms("1 year"),
includeSubdomains: true
}));
helmet 미들웨어와 ms 모듈을 설치했다. helmet을 통해 HSTS기능을 적용하고 ms를 통해 "2 days" 같이 사람이 쓰는 문자열을 밀리초로 변환 해줬다.
let's encrypt
기존에는 HTTPS를 사용하기 위해 인증기관에서 SSL을 구매해야 했지만, Mozilla, Cisco, EFF등이 모여 ISRG라는 SSL인증기관을 만들어 무료로 제공하겠다 했다. 이 곳에서 인증서를 발급받고 HTTPS를 적용하면 된다.
XSS(교차 사이트 스크립팅) 공격은 게시판이나 웹 메일 등에 자바 스크립트와 같은 스크립트 코드를 삽입 해 개발자가 고려하지 않은 기능이 작동하게 하는 공격이다.
예시
<!-- 자바스크립트 링크 -->
<a href="javascript:alert('XSS')">XSS</a>
<!-- 이벤트 속성 -->
<img src="#" onerror="alert('XSS')">
XSS 공격을 막는 방법은 다음과 같다.
// X-XSS-Protection 보안헤더 작성.
app.use(helmet.xssFilter());
CSRF attack(Cross Site Request Forgery)은 웹 어플리케이션 취약점 중 하나로 인터넷 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 만드는 공격이다.이를 예방하기 위해 매 요청마다 서버로 토큰을 이용한 인증을 해줘야 한다.
공격방식은 다음과 같다
post form등을 받을 때, 서버는 쿠키가 올바른 것을 확인하고 작업을 수행하는데, 이를 악용해 해커가 자신이 원하는 POST 요청을 수행하도록 할 수 있다. 다음의 스크립트로 자동으로 form을 제출한다고 생각해보자
<script>
var formElement = document.querySelector("form");
formElement.submit();
</script>
// 출처: https://jeong-pro.tistory.com/68 [기본기를 쌓는 정아마추어 코딩블로그]
이렇게 자기의 폼과 자동 제출을 넣은 것을 iframe으로 보이지 않게 해버린다.
<input name="_csrf" type="hidden" value="1dmkTnkHePmTB0d1glhm">
만약 서버에서 CSRF token을 유지하는 것이 힘들면 고려해볼 수 있는 대안이다. 구현하기 쉽고 stateless이다.
랜덤 값(csrf token)을 쿠키와 request parameter(혹은 header) 두 곳 모두에 담아 전송하고, 이 두 곳의 값이 일치하는지 항상 확인한다.
이는 웹브라우저의 Same Origin 정책으로 공격자는 쿠키 값에 접근할 수 없는 점을 이용한 것이다.
csrf secret: Cryptographically secure CSRF tokens. only known by the server.
csrf token: a hash of the secret and a salt.
다음은 csurf 미들웨어를 사용한 Double-Submit Cookie Pattern의 예시이다
var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
var express = require('express')
// setup route middlewares
var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })
// create express app
var app = express()
// parse cookies
// we need this because "cookie" is true in csrfProtection
app.use(cookieParser())
app.get('/form', csrfProtection, function (req, res) {
// pass the csrfToken to the view
res.render('send', { csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function (req, res) {
res.send('data is being processed')
})
// Form 예제
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
Favorite color: <input type="text" name="favoriteColor">
<button type="submit">Submit</button>
</form>
// Ajax 예제
<meta name="csrf-token" content="<%= csrfToken %>">
<form>
Name
<input type="text" name="name">
<button type='submit'>Submit</button>
</form>
<script>
const form = document.querySelector('form');
const input = document.querySelector('input');
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
form.onsubmit = async (e) => {
e.preventDefault();
const name = input.value;
const response = await fetch('/process', {
credentials: 'same-origin',
headers: {
'CSRF-Token': token,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify({ name })
})
const result = await response.text();
alert(result);
}
</script>
cookie를 사용하지 않아도 csurf를 사용할 수 있다. 이 경우 cookie가 아니라 req.session을 사용한다.
다만 서브 도메인에서 부모 도메인의 쿠키를 조작할 수 있고 일반 HTTP 연결을 통해 도메인의 쿠키가 설정(set)될 수 있으므로, 서브 도메인이 모두 보호되거나 HTTPS 연결만 수용하도록 해야 이 기술이 효과가 있다.
예를 들어 https://www.example.com
의 서브도메인 https://www.example.com/submit
에서 xss공격 스크립트를 이용하거나 meta 태그로 쿠키값을 조작할 수 있다.
<!-- xss공격 스크립트 -->
<script>document.cookie = “_csrf=a; Path=/submit; domain=example.com”;</script>
<!-- meta 태그 -->
<meta http-equiv="set-cookie" content="_csrf=a; Path=/submit; domain=example.com">
// Express가 사용되었다는 것을 숨긴다.
app.disable("x-powered-by");
클릭재킹은 사용자의 클릭을 가로채 다른 것을 누르게 하는 방식이다.
제한적인 X-Frame-Option을 보내서 브라우저가 더이상 이 프레임을 로드하지 않도록 해 대응할 수 있다.
X-Frame-Option
// X-Frame-Option
app.use(helmet.frameguard("deny"));
플래시 플레이어와 리더 같은 어도비 제품은 교차 사이트 웹 요청을 일으킬 수 있다.
이는 루트에 crossdomain.xml이라는 파일을 추가해서 막을 수 있다.
어도비 제품은 도메인 밖의 파일을 로드할 때, 먼저 우리 도메인이 허용하는지 확인하기 위해 crossdomain.xml파일을 살펴본다.
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-control permitted-cross-domain-policies="none">
</cross-domain-policy>
위 내용을 가진 crossdomain.xml을 만들어서 루트경로에 놓으면 플래시 사용자가 우리 도메인에서 요청하지 않는 한 우리 사이트 밖에서 콘텐츠를 로드하지 못하도록 막는다.
MIME(Multipurpose Internet Mail Extensions) 타입이 없을 때, 혹은 클라이언트가 타입이 잘못 설정됐다고 판단한 어떤 다른 경우에, 브라우저들은 MIME 스니핑(sniffing)을 시도할 수도 있는데, 이는 리소스를 훑어보고 정확한 MIME 타입을 추측하는 것이다.
많은 브라우저에서 파일 형식이 자바스크립트용이 아니라면 실행하도록 허용하는데, 만약 굉장히 위험한 파일이 html과 유사하다면 이 파일을 html로 해석할 것이다.
MIME 스니핑은 X-Content-Type-Options 헤더를 nosniff라는 옵션으로 설정해 차단할 수 있다.
// Helmet을 사용한 HTTP 헤더를 설정
app.use(helmet.noSniff());