최근 Spring Boot, SvelteKit을 이용해서 사이드 프로젝트를 몇 개 살펴봤다. 프로젝트에서는 프론트를 Sveltekit을 SSR (Server-side Rendering) 모드로만 사용했다. Client-Side Rendering 기능은 꺼놓은 상태였다.

이들 사이트에서 서버 사이드 렌더링(SSR)에서 쓰이는 데이터가 외부로 노출되는 것을 발견했다. 개발자가 부주의 하다면 민감한 정보가 외부로 노출 될 수 있다.

CSR 에서는 (당연히) 모든 데이터가 클라이언트에게 전송됨

CSR이 켜져있다면 중요한 데이터가 바깥으로 새어나갈 수 있다. 컴포넌트 단으로 넘어갈 데이터는 모두 클라이언트에게 전송되기 때문이다. 아래의 예시 코드를 보자.

+page.svelte

<script>
   let userData = data()
   let isAdmin = checkAdmin();
</script>

{#if isAdmin}
<div>
   관리자만 볼 수 있는 중요한 정보 {userData)
</div>
{/if}

Client측에서 이 페이지를 확인하기 위해서는 isAdmin이 바깥으로 나가게 된다. 그런데 예상치 못하게 userData 또한 바깥으로 나가게 된다. 데이터 값에 따라서 화면을 가리는 방식으로 처리하려는 시도를 할 수가 없게 된다.

위의 예시는 누가 그래~ 할 수도 있다. 아래의 예시를 보자. 서버에서 글 정보를 가져오는 것은 같다. 편의를 위해서 백앤드에 요청시 DB의 row를 통째로 들고온다. 이것을 관리자 여부에 따라서 표출하냐, 마냐의 문제만을 가진다.

+page.svelte

<script>
   let { data } = $props();
   let article = data.article
   let isAdmin = data.isAdmin
</script>

<header>
   {article.title}
   {#if isAdmin}
      차단 횟수 {article.user.bancount}
      글 작성 IP {article.ip}
   {/if}
</header>
<article>
   {article.body}
</article>

PHP나 Spring Thymleaf와 같은 전통적인 SSR에서는 데이터가 밖으로 나가지 않는다. 그러나, Sveltekit와 같은 프레임워크에서는 데이터가 밖으로 나갈 수 있다.

그러나 SvelteKit과 같은 프레임워크의 SSR은 그렇지 않음.

문제는 요즘 자주 쓰이는 Sveltekit 같은 프론트 프레임워크는 “정보를 클라이언트에게 보낸다”는 것이다. 이들 프레임워크는 아무리 SSR 모드를 켜고 CSR를 꺼도 hydration을 위한 endpoint는 남아있다. 이 곳에 요청하면 서버 내부 렌더링을 위해 쓰인 데이터가 모두 클라이언트에게 넘어간다.

아래는 CSR이 꺼져있는 Sveltekit 서버이다. CSR이 꺼져 있음에도 불구하고__data.json 는 남아있다. 악의적인 사용자가 이 곳을 요청하면 서버 렌더링에만 쓰인 데이터가 튀어나온다. 이 자료는 load 함수에서 만든 모든 데이터를 담고있다.

뷰 단에서 단순히 보이지 않게 하는것이 통하지 않는다. 이미 서버단에서 요청이 다 나가기 때문이다.

이 문제는 hydration이 있는 프레임워크에서 공통적으로 발생한다. 이런 프레임워크는 렌더링용 정보를 언제든 바깥으로 내보낼 준비를 해야하기 때문이다.

그러면 어떻게 해야하는가?

SSR를 지원하는 SPA 계열 프레임워크에서는 서버 렌더링용 데이터가 바깥으로 나갈 수 있음을 항시 염두해야 한다. 그러므로, 애초에 권한에 따라서 데이터를 마스킹 해야 한다.

백엔드에서 DTO를 잘게 쪼개고 권한별로 마스킹하는 정석 방법

가장 좋은것은 백앤드단에서 데이터를 정리해서 보내는 것이다, 그러나 여건상 어려울 때가 많다. 특히 DB를 직접 가져오거나, Java와 같이 타입이 딱딱하다면 어려울 가능성이 높다. 모든 조건에 따라서 적용 View Class를 만들어야 할수도 있기 때문이다.

예를 들어, 사용자의 계정 정보를 가져오는 API를 만들었다고 하자. 필요없는 필드를 아예 가리기 위해서는 Account, AccountMyView, AccountModeratorView, AccountSubadminView, AccountAdminView와 같이 수많은 타입이 필요할 수 있다. 그럼에도 불구하고, API를 요청하기전 까지는 조건을 확정하기 힘들 수 도 있다.

front 프레임워크에서 데이터를 가리는 방법

시간이 많다면 백앤드 개발시 부터 이것들을 고려하면 좋다. 그것이 확실한 방법이다. 그러나 여건상 어렵다면 SSR을 위해 데이터를 제공하는 코드단에서 일부 필드를 지워야 한다.

미들웨어 급에서 요청을 막는 방법

그것마저도 어렵다면 __data.json과 같은 요청을 아예 막는 방법도 있다. sveltekit의 경우, 아래와 같은 조치를 사용할 수 있다. 아래 코드는 data용 요청을 모두 막는다. CSR이 결합되어 있다면 쓰기 어려울 수 있다.

src/hooks.server.ts

/** @type {import('@sveltejs/kit').[Handle](https://kit.svelte.dev/docs/types#public-types-handle)} */
export async function handle({ event, resolve }) {
  // true when the request is for a `__data.json` endpoint
  // https://kit.svelte.dev/docs/types#public-types-requestevent
  if (event.isDataRequest) {
    return new Response(null, { status: 400 });
  }

  const response = await resolve(event);
  return response;
}

단, 이 경우 전통적인 SSR 방식밖에 사용이 불가능하다. hydration가 정말로 불필요하다면 고려해 보자.

SvelteKit외에도 이러한 문제가 발생할 수 있는가?

필자는 Sveltekit만을 확인했다. 다른 곳에서도 동일하게 발생할 것으로 예상되나, 확실하진 않다. 아래의 ChatGPT o1 답변을 참고하자면 hydration을 지원하는 모든 곳에서 공통적으로 발생할 것 같다.

  • SvelteKit: 글에서 주로 다룬 사례.
  • Next.js(React 기반): getServerSideProps, getStaticProps 등을 통해 페이지에 데이터를 주입하면, 최종적으로 hydration 과정에서 클라이언트에 해당 데이터가 노출될 수 있습니다.
  • Nuxt(Vue 기반): useFetch나 asyncData로 받아온 데이터가 클라이언트 측에 전송될 수 있습니다.
  • Remix(React 기반): loader에서 반환한 데이터가 클라이언트에도 전달됩니다.
  • Astro: 부분적으로만 hydration을 켜놓을 수도 있으나, SSR + hydration을 동시에 사용한다면 유사 문제가 발생할 수 있습니다.

즉, “SSR + (부분)hydration”을 동시에 지원하는 프레임워크들은 대부분 비슷한 구조로 동작하므로, 민감 정보가 뷰 렌더링 용도로만 쓰인다고 하더라도 그대로 노출될 위험이 있다는 점은 전부 같습니다.

ChatGPT o1 (2025-03-08)

끝내며

이 문제점도 지인의 사이트를 탐색하다가 발견한 것이다. CSR을 끄고 SSR만 켜서 보안상 문제가 없다고 생각했음에도 이런 문제가 발생했다. 사실 이런게 가능하다는것 자체를 인지하지 못했다.

이 글에서는 블로그 정보만을 표시했지만, 실제 상황에서는 계정 정보를 통째로 보여줬었다. 내 정보를 표시하기 위해 세션 데이터를 사용했었다. 세션 데이터에 가입 일자, 최근 로그인 일자 및 IP등을 비롯한 여러 데이터가 숨겨져 있었다. 이 정보들이 밖으로 튀어나온 것이다.

이러한 웹 프레임워크를 쓸 때 반드시 확인해 보자.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

댓글을 작성하기 위해 아래의 숫자를 입력해 주세요. *Captcha loading…

최신 글

목차