들어가며
Next.js 환경의 프론트엔드 프로젝트 경험을 되돌아보며, 사전에 좀 더 깊게 고려했으면 좋았을 보안 관련 내용들을 알아보고 정리해 보려 합니다.
XSS 공격
⬇️ 설명
XSS 공격은 가장 흔한 공격 중 하나로 입력 필드나 URL 파라미터 등을 통해 스크립트를 코드에 삽입하고 다른 사용자의 브라우저에서 해당 스크립트가 실행되도록 만드는 공격입니다. 쿠키 탈취, 하이재킹 등 심각한 문제로 이루어질 수 있습니다.
XSS 유형을 아래처럼 3가지로 나눌 수 있습니다.
- Reflected XSS
- 악성 스크립트를 URL 파라미터나 다른 입력값에 포함해 사용자에게 전달하고, 사용자가 이 악성 스크립트가 포함된 URL을 클릭하거나 폼을 제출하면 서버가 해당 스크립트를 응답 페이지에 그대로 포함해 사용자의 브라우저에서 실행되게 하는 방식입니다. URL 예시)
http://example.com/search?query=<script>alert('XSS');</script>
- 악성 스크립트를 URL 파라미터나 다른 입력값에 포함해 사용자에게 전달하고, 사용자가 이 악성 스크립트가 포함된 URL을 클릭하거나 폼을 제출하면 서버가 해당 스크립트를 응답 페이지에 그대로 포함해 사용자의 브라우저에서 실행되게 하는 방식입니다. URL 예시)
- Stored XSS
- 악성 스크립트를 웹 애플리케이션의 데이터베이스나 파일 등 서버 측에 영구적으로 저장시키는 방식입니다. 공격자는 게시판, 댓글, 사용자 프로필 등 사용자의 입력값을 서버에 저장하는 기능에 악성 스크립트 (
<script>alert('XSS');</script>
등)를 포함해서 입력하면 데이터베이스 등에 저장되어 다른 사용자에게 악성 스크립트가 포함된 데이터를 전송할 수 있습니다. 한 번의 공격으로 다수의 사용자를 감염시킬 수 있어 파급력이 가장 큽니다.
- 악성 스크립트를 웹 애플리케이션의 데이터베이스나 파일 등 서버 측에 영구적으로 저장시키는 방식입니다. 공격자는 게시판, 댓글, 사용자 프로필 등 사용자의 입력값을 서버에 저장하는 기능에 악성 스크립트 (
- DOM based XSS
- 브라우저의 DOM 환경에서 발생하는 공격입니다. 악성 스크립트가 포함된 데이터가 DOM 내에서 조작되는 과정에서 실행됩니다. 클라이언트 측 스크립트의 취약점으로 인해 발생합니다. 따라서 공격 페이로드가 서버로 전송되지 않을 수 있어 서버 로그에 남지 않고, 서버 측 보안 장비로 탐지하기 어려울 수 있습니다.
🧚♂️ 해결책
그렇다면 여기서 FE 작업자는 XSS 대응을 어떻게 해야 할까요?
Next.js 환경에서 살펴보면 Next.js는 React 기반이기 때문에 jsx를 사용하여 변수를 렌더링할 때 기본적으로 문자열을 Escape 처리합니다.
여기서 중요한 게 dangerouslySetInnerHTML
인데요, 이 Escape 기능을 의도적으로 비활성화해 주는 코드이기 때문에 사용 시 매우 신중해야 합니다.
dangerouslySetInnerHTML
을 사용하는 경우 dompurify
와 같은 라이브러리를 사용해서 잠재적으로 위험한 스크립트나 속성을 제거해 줘야 합니다.
import DOMPurify from 'isomorphic-dompurify';
// 서버 또는 클라이언트에서 dirtyHtml을 받음
const RenderHtmlContent = ({ dirtyHtml }) => {
// DOMPurify 통해 정화
const cleanHtml = DOMPurify.sanitize(dirtyHtml);
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}
CSP 추가 설정으로 좀 더 예방해 보겠습니다.
CSP는 브라우저가 로드하고 실행할 수 있는 리소스(script,css 등)를 제어하는 강력한 HTTP 헤더 기반 보안 정책입니다.
next.config.js 파일의 headers에 추가해서 사용해 봅시다.
module.exports = {
async headers() {
return [
{
// 모든 경로에 이 헤더를 적용합니다.
// 특정 경로에만 적용하려면 source를 구체적으로 명시하세요. (예: '/admin/:path*')
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' *.yourdomain.com; style-src 'self' 'unsafe-inline'; img-src * blob: data:; media-src 'none'; connect-src *; font-src 'self';"
// default-src 'self': 모든 리소스는 기본적으로 같은 출처(도메인)에서만 로드됩니다.
// script-src 'self' 'unsafe-inline' 'unsafe-eval': 자바스크립트는 같은 출처, 인라인 스크립트 및 eval() 함수 사용을 허용합니다.
// style-src 'self' 'unsafe-inline': CSS는 같은 출처 및 인라인 스타일을 허용합니다.
// img-src 'self' data: blob:: 이미지는 같은 출처, data URI 및 blob URL에서 로드할 수 있습니다.
// font-src 'self': 폰트는 같은 출처에서만 로드됩니다.
// connect-src 'self' https://*.koreatimes.co.kr: AJAX, WebSocket 등의 연결은 같은 출처 및 koreatimes.co.kr 서브도메인에 대해 허용됩니다.
},
],
},
];
},
};
CSP 설정은 신중하게 설정해야 하는데요, 너무 엄격하면 필요한 스크립트나 스타일이 로드되지 않을 수 있고, 너무 느슨하면 효과가 없습니다. unsafe-inline
이나 unsafe-eval
은 보안상 취약하므로 꼭 필요한 경우가 아니라면 피해서 사용해야 합니다.
CSRF
⬇️ 설명
사용자가 로그인된 상태에서, 악성 웹사이트나 이메일 링크 등을 통해 사용자의 의도와는 상관없이 서버에 특정 요청(글 삭제, 정보 수정 등)을 보내게 만드는 공격입니다.
🧚♂️ 해결책
next.config.js에 CSRF 헤더 설정을 추가해 보겠습니다.
module.exports = {
async headers() {
return [
{
// 모든 경로에 이 헤더를 적용합니다.
// 특정 경로에만 적용하려면 source를 구체적으로 명시해야 합니다. (예: '/admin/:path*')
source: '/(.*)',
headers: [
{
key: "X-Content-Type-Options",
value: "nosniff"
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin"
},
{
key: "Set-Cookie",
value: "Path=/; HttpOnly; Secure; SameSite=Strict"
}
],
},
];
},
};
클릭 재킹 (Clickjacking)
⬇️ 설명
mdn 문서에서는 아래처럼 설명하고 있습니다.
클릭재킹(Clickjacking)은 웹사이트 사용자를 속여 자신도 모르게 악성 링크를 클릭하도록 하는 인터페이스 기반 공격입니다.
예를 들면 투명한 <iframe>
등을 페이지 위에 교묘하게 겹쳐놓고, 사용자가 의도하지 않은 버튼이나 링크(예: 광고 클릭, 좋아요 버튼)를 클릭하도록 유도하는 공격입니다.
🧚♂️ 해결책
X-Frame-Options
HTTP 헤더를 설정하여 다른 웹사이트에서 페이지 위에 <iframe>
을 추가하는 것을 방지할 수 있습니다.
next.config.js 파일의 headers에 추가해 보도록 하겠습니다.
module.exports = {
async headers() {
return [
{
// 모든 경로에 이 헤더를 적용합니다.
// 특정 경로에만 적용하려면 source를 구체적으로 명시하세요. (예: '/admin/:path*')
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY', // 또는 'SAMEORIGIN'
},
],
},
];
},
};
value는 'DENY' 또는 'SAMEORIGIN'에서 사용할 수 있습니다.
DENY
: 가장 강력한 설정으로, 어떤 웹사이트에서도 작업한 페이지를 프레임 안에 넣을 수 없습니다. 클릭재킹 공격을 방지하는 가장 확실한 방법입니다.SAMEORIGIN
: 웹사이트 내부의 다른 페이지에서는 프레임 안에 넣을 수는 있지만, 외부 웹사이트에서는 불가능합니다.
민감한 데이터 노출
브라우저에서 실행되는 코드나 저장소에 민감한 정보(API, 비밀 키 등)가 남아있으면 해킹에 취약하기 때문에 조심해야 합니다.
.env 파일에서 환경 변수 사용 시 NEXT_PUBLIC
접두사에 포함해서 많이 사용하는데요, 이러면 번들된 js파일에 내용이 포함되기 때문에 누구나 볼 수 있습니다.
로컬 개발 환경에서는 .env를 사용해서 확인하지만 반드시 .gitignore 파일에 추가하여 저장소에 올라가지 않도록 합니다.
# .gitignore
# Environment variables
.env
.env.*
!.env.example
vercel과 같은 플랫폼으로 배포할 시 플랫폼에서 제공하는 환경 변수 설정에서 프로덕션용 배포 key를 입력하여 사용하면 됩니다.
마치며
평소 프론트엔드 개발 시 놓치기 쉬운 보안 사항들을 정리할 필요성을 느꼈는데, 이번에 관련 내용을 살펴보면서 정리할 수 있어 개인적으로 좋은 시간이었습니다.
읽어주셔서 감사합니다.