14.1 리액트에서 발생하는 크로스 사이트 스크립팅(XSS)
웹사이트 개발자가 아닌 제3 자가 웹사이트에 악성 스크립트를 삽입해 실행할 수 있는 취약점을 의미한다.
script 가 실행될 수 있다면 웹사이트 개발자가 할 수 있는 모든 작업을 함께 수행할 수 있으며, 쿠키를 획득해 사용자의 로그인 세션 등을 탈취하거나 사용자의 데이터를 변경하는 등 각종 위험성이 있다.
14.1.1 dangerouslySetlnnerHTMLprop
특정 브라우저 DOM 의 innerHTML을 특정한 내용으로 교체할 수 있는 방법이다.
dangerouslySetlnnerHTML은 오직 __ html 을 키를 가지고 있는 객체만 인수로 받을 수 있으며, 이 인수로 넘겨받은 문자열을 DOM에 그대로 표시하는 역할을 한다. 그러나 이 dangerouslySetlnnerHTML 의 위험성은 dangerouslySetlnnerHTML 이 인수로 받는 문자열에는 제한이 없다는 것이다. 다음 예제를 보자.
const html = `<span><svg/onload=alert(origin)></span>`
function App() {
return <div dangerouslySetInnerHTML = {{__html: html}} />
}
export default App
위 코드를 방문하면 다음과 같이 origin이 alert 로 나타나게 된다.
말 그대로 dangerouslySetlnnerHTML은 사용에 주의를 기울여야 하는 prop 이며, 여기에 넘겨주는 문자열 값은 한번 더 검증이 필요하다는것을 알수있다.
14.1.2 useRef를 활용한 직접 삽입
dangerouslySetlnnerHTML과 비슷한 방법으로 DOM에 직접 내용을 삽입할 수 있는 방법으로 useRef가 있다.
useRef를 활용하면 직접 DOM 에 접근할 수 있으므로 이 DOM 에 앞서와 비슷한 방식으로 innerHTML 에 보안취약점이 있는 스크립트를 삽입하면 동일한 문제가 발생한다.
<script> 나 svg/onload 를 사용하는 방식 외에도 <a> 태그에 잘못된 href 를 삽입하거나 onclick, onload 등 이벤트를 활용하는 등 여 러 가지 방식의 xss가 있지만 공통적인 문제는 웹사이트 개발자가 만들지 않은 코드를 삽입한다는 것에 있다.
14.1.3 리액트에서 XSS 문제를 피하는 방법
제3자가 삽입 할 수 있는 HTML을 안전한 HTML 코드로 한 번 치환하는 것이다.
이러한 과정을 새니타이즈(sanitize) 또는 이스케이프(escape) 라고 하는데, 새니타이즈를 직접 구현해 사용하는 등 다양한 방법이 있지 만 가장 확실한 방법은 npm 에 있는 라이브러리를 사용하는 것이다.
이와 관련된 유명한 라이브러리로는 다음과 같은 것이 있다.
- DOMpurity( https://github.com/cure53/DOMPurify)
- sanitize-html(https://github.com/apostrophecms/sanitize-html)
- js-xss( https://github.com/leizongmin/js-xss)
sanitize-html은 허용할 태그와 목록을 일일히 나열하는 이른바 허용 목록 (allow list) 방식이다.
이러한 치환 과정은 되도록 서버에서 수행하는 것이 좋다. 서버는 '클라이언트에서 사용자가 입력한 데이터는 일단 의심한다'라는 자세로 클라이언트의 POST 요청에 있는 HTML을 이스케이프하는 것이 제일 안전하다.
쿼리스트링에 있는 내용을 그대로 실행하거나 보여주는 경우에도 보안 취약점이 발생할 수 있다.
따라서 개발자는 자신이 작성한 코드가 아닌 query, GET 파라미터, 서버에 저장된 사용자가 입력한 데이터 등 외부에 존재하는 모든 코드를 위험한 코드로 간주하고 이를 적절하게 처리하는 것이 좋다.
리액트의 JSX 데이터 바인딩
dangerouslySetlnnerHTML이라는 속성이 별도로 존재하는 이유는 기본적으로 리액트가 XSS를 방어하기 위해 이스케이프 작업을 하기 때문이다.
따로 이스케이프 작업을 하지 않아도 실제로 실행되지 않는다. 즉, <div>{html}</div>와 같이 HTML에 직접 표시되는 textContent와 HTML 속성 값에 대해서는 리액트가 기본적으로 이스케이프 작업을 해주는 것을 알 수 있다.
그러나 dangerouslySetlnnerHTML이나 props로 넘겨받는 값의 경우 개발자의 활용도에 따라 원본 값이 필요할 수 있기 때문에 이러한 작업이 수행되지 않는다.
14.2 getServerSideProps와 서버 컴포넌트를 주의하자
서버에는 일반 시용자에게 노출되면 안 되는 정보들이 담겨 있기 때문에 클라이언트, 즉 브라우저에 정보를 내려줄때는 조심해야 한다.
getServerSideProps 에서 cookie 정보를 가져온 다음 이를 클라이언트 리액트 컴포넌트에 문자열로 제공해 클라이언트에서 쿠키의 유효성에 따라 이후 작업을 처리한다. 이 코드가 보안이 안 좋은 이유는 getServerSideProps 가 반환하는 props 값은 모두 사용자의 HTML에 기록되고, 또한 전역 변수로 등록되어 스크립트로 충분히 접근할 수 있는 보안 위협에 노출되는 값이 되기 때문이다.
충분히 getServerSideProps에서 처리할 수 있는 리다이렉트가 클라이언트에서 실행되어 성능 측면에서도 손해를 본다. 따라서 getServerSideProps 가 반환하는 값 또는 서버 컴포넌트가 클라이언트 컴포넌트에 반환히는 props는 반드시 필요한 값으로만 철저하게 제한되어야 한다. 이는 보안 측면의 이점뿐만 아니라 성능 측면에서도 이점을 가져다줄수 있다. 다음과 같이 수정하면,
쿠키 전체를 제공하는 것이 아니라 클라이언트에서 필요한 token 값만 제한적으로 반환했고, 이 값이 없을 때 예외 처리할 리다이렉트도 모두 서버에서 처리했다. 이로써 불필요하게 쿠키 값을 노출하는 것을 없앴고,리다이렉트 또한 한층 빨라질 것이다.
이러한 방식의 접근법은 비단 getServerSideProps 와 서버 컴포넌트뿐만 아니라 리덕스에서 서버 사이드에서 가져온 상태로 가져오는 window. __ PRELOADED_STATE 와 같은 값을 데이터 를 초기화할 때도 적용된다. window. PRELOADED_STATE __ 의 값은 XSS 에 취약 할 수 있기 때문에 반드시 새니타이즈를 거치고, 꼭 필요한 값만 제공해야 한다.
14.3 <a> 태그의 값에 적절한 제한을 둬야 한다
<a> 태그의 href 에 javascript: 로 시작히는 자바스크립트 코드를 넣어둔 경우를 본 적이 있을 것이다. 이는 주로 <a> 태그의 기본 기능 즉 href 로 선언된 URL로 페이지를 이동하는 것을 막고 onClick 이벤트와 같이 별도 이벤트 핸들러만 작동시키기 위한 용도로 사용된다.
하지만 <a> 태그는 반드시 페이지 이동이 있을 때만 사용하는 것이 좋다.
왜냐하면 href가 작동하지 않는 것이 아니고, href 내에 자바스크립트 코드가 존재한다면 이를 실행한다는 뜻이다. 따라서 코드를 실행하면경고문과 함께 정상적으로 렌더링되는 것을 확인 할 수 있다.
XSS에서 소개한 사례와 비슷하게, href 에 사용자가 입력한 주소를 넣을 수 있다면 이 또한 보안 이슈로 이어질 수 있다. 따라서 href 로 들어갈 수 있는 값을 제한해야 한다. 그리고 피싱 사이트로 이동하는 것을 막기 위해 가능하다면 origin 도 확인해 처리하는 것이 좋다.
function isSafeHref(href:string) {
let isSafe = false
try {
//javascript:가 오면 protocol이 javascript:가 된다.
const url = new URL(href)
if(['http:', 'https:']).includes(url.protocol)){
isSafe = true
}
} catch {
isSafe = false
}
return isSafe
}
위에 함수를 만들어서 안전한 주소면 true, 위험한 주소면 false를 반환하여 처리한다.
14.4 HTTP 보안 헤더 설정하기
14.4.1 Strict-Transport-Security
HTTP의 Strict-Transport-Security 응답 헤더는 모든 사이트가 HTTPS를 통해 접근해야 하며, 만약 HTTP로 접근하는 경우 이러한 모든 시도는 HTTPS로 변경되게 한다. 사용법은 다음과 같다.
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
<expire-time>은 이 설정을 브라우저가 기억해야 하는 시간을 의미하며, 초 단위로 기록된다. 이 기간 내에 HTTP로 사용자가 요청해도 브라우저는 이 시간을 기억했다가 자동으로 HTTPS로 요청하게 된다. 이 시간이 경과하면 HTTP로 로드를 시도한 뒤 응답에 따라 HTTPS로 이동하는 등의 작업을 수행한다. 시간이 0으로 돼 있다면 헤더가 즉시 만료되고 HTTP로 요청하게 된다. 권장값은 2년이다.
14.4.2 X-XSS-Protection
X-XSS-Protection은 비표준 기술로, 현재 사파리와 구형 브라우저에서만 제공되는 기능이다.
이 헤더는 페이지에서 xss 취약점이 발견되면 페이지 로딩을 중단하는 헤더다. Content-Security-Policy를 지원하지 않는 구형 브
라우저에서 사용이 가능하다. 반드시 페이지 내부에 XSS에 대한 처리가 존재하는지 확인해야 한다.
14.4.3 X-Frame-Options
X-Frame-Options 는 페이지를 frame , iframe, embed , object 내부에서 렌더링을 허용할지를 나타낼 수 있다.
X-Frame-Options: DENY 제3의 페이지 내부에 사이트를 삽입하려고 하면 무조건 막는다.
X-Frame-Options: SAMEORIGIN 같은 origin에 대해서만 프레임을 허용한다.
14.4.4 Permissions-Policy
웹사이트에서 사용할 수 있는 기능과 사용할 수 없는 기능을 명시적으로 선언하는 헤더다.
예를 들어, 브라우저에서 사용자의 위치를 확인하는 기능(geolocation)과 관련된 코드를 작성하지 않고 별도로 차단하지 않는다면 XSS 공격 등으로 이 기능을 취득해 사용자의 위치를 획득하는 위험이 있다. 이 헤더를 활용하여 사용자에게 미칠 수 있는 악영향을 제한할 수 있다.
제어할 수 있는 목록은 MDN 문서 또는 https://www.permissionspolicy.com/에서 확인할 수 있다.
14.4.5 X-Content-Type-Options
먼저 MIME에 대한 이해가 필요한데, MIME란 Multipurpose Internet Mail Extenstions의 약자로 Content-type의 값으로 사용된다.
원래는 메일을 전송할 때 사용하던 인코딩 방식으로, 현재는 Content-type에서 대표적으로 사용되고 있다.
네이버에서는 www.naver.com 을 Content-Type: text/html; charset=UTF-8 로 반환해 브라우저가 이를 UTF-8로 인코딩된 text/html 로 인식할 수 있게 도와주고, 브라우저는 이 헤더를 참고해 해당 파일에 대해 HTML을 파싱하는 과정을 거치게 된다. 이러한 MIME은 jpg, CSS, JSON 등 다양하다.
X-Content-Type-Options 란 Content-type 헤더에서 제공하는 MIME 유형이 브라우저에 의해 임의로 변경되지 않게 하는 헤더다.
예를들어, 공격자가 .jpg 파일을 웹서버에 업로드 했는데 실제로 그림 관련 정보가 아닌 악의적인 스크립트가 담겨 있음에도 해당 코드를 실행하는 위험이 있다. 이런 경우 다음과 같은 헤더를 설정해 두면 파일의 타입이 css 나 MIME 이 text/css 가 아닌 경우, 혹은 파일 내
용이 script나 MIME 타입이 자바스크립트 타입이 아니면 차단하게 된다.
X-Content-Type-Options: nosniff
14.4.6 Referrer-Policy
HTTP 요청에는 Referer 라는 헤더가 존재하는데, 이 헤더에는 현재 요청을 보낸 페이지의 주소가 나타난다. 이 헤더는 사용자가 어디서 와서 방문중인지 인식 할 수 있는 헤더지만, 반대로 사용자 입장에서는 원치 않는 정보가 노출될 위험도 존재한다.
Referrer-Policy 헤더는 이 Referer 헤더에서 사용할 수 있는 데이터를 나타낸다.
Referer 에 대해 이야기할 때는 출처(origin)를 빼놓을 수 없다.
먼저 https://yceffort.kr 이라는 주소의 출처는 다음과 같이 구성돼 있다.
- scheme: HTTPS 프로토콜을 의미한다.
- hostname: yceffort. kr 이라는 호스트명을 의미한다.
- port: 443 포트를 의미한다.
위 세 가지 조합을 출처라고 한다.
그리고 두 주소를 비교할때 same-origin 인지, cross-origin 인지는 다음과 같이 구분할 수 있다.
yceffort.kr:443을 기준으로 비교했을 때 다음과 같이 나타낼 수 있다.
Referrer-Policy는 응답 헤더 뿐만 아니라 페이지의 <meta/> 태그로도 다음과 같이 설정할 수 있다.
<meta name="referrer" content="origin" />
페이지 이동 시나 이미지 요청, link 태그 등에도 다음과 같이 사용할 수 있다.
<a href="http://yceffort.kr" referrerpolicy="origin">...<a/>
구글에서는 이용자의 개인정보 보호를 위해 strict-origin-when-cross-origin 혹은 그 이상을 명시적으로 선언해 둘 것을 권고한다.
meta태그 안에 설정할 때는 index.html 파일의 head 태그 내부에 추가하면 된다.
14.4.7 Content-Security-Policy
콘텐츠 보안 정책 (Content-Security-Policy, 이하 CSP)은 XSS 공격이나 데이터 삽입 공격과 같은 다양한 보안 위협을 막기 위해 설계됐다. 지시문이 굉장히 많으며, 다음은 대표적으로 이용되는 지시문이다. 모든 지시문은 웹 표준을 정의한 W3에서 확인 가능하다.
*-src
font-src , img-src, script-src 등 다양한 src 를 제어할 수 있는 지시문이다.
Content-Security-Policy: font-src < source>;
Content-Security-Policy: font-src < source> < source>;
위처럼 선언해 두면 font 소스만 가져올 수 있고 외에는 모두 차단된다.
비슷한 유형의 지시문은 다음과 같다.
만약 해당 -src가 선언돼 있지 않다면 default-src로 한번에 처리할 수도 있다.
Content-Security-Policy: default-src < source>;
Content-Security-Policy: default-src < source> < source>;
form-action
form 양식으로 제출할 수 있는 URL을 제한할 수 있다. 다음과 같이 form-action 자체를 모두 막아버리는 것도 가능하다.
위에 submit을 눌러 form을 제출하면 콘솔에 다음과 같은 에러메시지와 함께 작동하지 않는다.
14.4.8 보안헤더 설정하기
Next.js
Next.js에서는 애플리케이션 보안을 위해 HTTP 경로별로 보안 헤더를 적용할 수 있다. next.config.js에서 다음과 같이 추가할 수 있다.
const securityHeaders = [
{
key: 'key',
value: 'value',
},
]
module.exports = {
async headers() {
return [
{
//모든 주소에 설정한다.
source: '/:path*',
headers: securityHeaders,
},
]
},
}
여기서 설정할 수 있는 값은 다음과 같다.
NGINX
정적인 파일을 제공하는 NGINX의 경우 다음과 같이 경로별로 add_header 지시자를 사용해 원하는 응답 헤더를 추가할 수 있다.
14.4.9 보안 헤더 확인하기
현재 웹사이트의 보안 헤더를 확인할 수 있는 가장 빠른 방법은 보안 헤더의 현황을 알려주는 https://securityheaders.com/을 방문하는 것이다. 헤더를 확인하고 싶은 웹사이트의 주소를 입력하면 보안 헤더 상황을 알 수 있다.
14.5 취약점이 있는 패키지의 사용을 피하자
패키지들은 버전에 따라 보안 취약점이 존재할 수도 혹은 업데이트 이후에 보안 취약점이 새롭게 드러나거나 파악되지 않았던 취약점이 나타날 수도 있다. 따라서 깃허브의 Dependabot이 발견한 취약점은 필요하다면 빠르게 업데이트해 조치해야 한다.
https://security.snyk.io/를 방문해 사용 중인 패키지 이름으로 검색해 보면 현재 라이브러리의 취약점을 한눈에 파악할 수 있다.
14.6 OWASP Top 10
OWASP 은 Open Worldwide (Web) Application Security Project 라는 오픈소스 웹 애플리케이션 보안 프로젝트를 의미한다.
주로 웹에서 발생할 수 있는 정보 노출, 악성 스크립트, 보안 취약점 등을 연구하며, 주기적으로 10대 웹 애플리케이션 취약점을 공개하는데 이를 OWASP Top 10 이라고 한다.
보안 취약점을 요약해 주는 것뿐만 아니라 이 문제를 어떻게 조치해야 하는지도 자세히 소개한다.
현재 개발 중인 개인프로젝트로 테스트 해봤다.
https://securityheaders.com/에 들어가서 사이트 주소 입력하니 나온 처참한 결과 ㅋㅋㅋㅋㅋㅋ
Strict-Transport-Security는 supabase를 써서 자동으로 적용이된 것 같았다.
헤더 옵션은 서버에서 설정해야 하는데 React는 서버가 없어서 사용하고 있는 배포 프로그램에 넣어주면 된다고 한다. 나는 vercel로 배포해서 다음과 같이 vercel 설정 파일을 만들어 넣어줬다.
//index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="strict-origin" />
<title>My Library</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
위에 말한 것처럼 referrer-policy를 meta 태그에 설정하는 방법으로 head태그 안에 추가하면 된다.
그런데 이렇게 html에 추가만 하니 적용이 안됐다.
아래 vercel.json에 Referrer-Policy: strict-origin 헤더를 추가해야만 적용되는거였다 !
//vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Referrer-Policy",
"value": "strict-origin"
},
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{
"key": "Content-Security-Policy",
"value": "font-src 'none'"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=(), fullscreen=(self)"
}
]
}
]
}
마지막 X-Frame-Options는 iframe이나 그런걸 안 써서 안 했는데 해야 되나용?
'React-study > dil' 카테고리의 다른 글
[모던 리액트 Deep Dive] 4장 서버 사이드 렌더링 (0) | 2025.01.10 |
---|---|
[모던 리액트 Deep Dive] 마지막 15장 (4) | 2024.12.26 |
[모던 리액트 Deep Dive] 13장 웹페이지 성능을 측정하는 다양한 방법 (1) | 2024.12.21 |
[모던리액트 Deep Dive] 12장 모든 웹 개발자가 관심을 가져야 할 핵심 웹 지표 (3) | 2024.12.20 |
[모던리액트 Deep Dive] 11장 Next.js 13과 리액트 18 (1) | 2024.12.10 |