프론트엔드 코드에 숨어있는 크리덴셜 유출 위협

Credential Leak Threat

크리덴셜 보안에 대한 큰 위협

여러분은 크리덴셜의 중요성에 대해 얼마나 알고 계신가요? 크리덴셜이란 애플리케이션이나 시스템에 접근할 수 있는 권한을 의미하는 것으로, API 키, 데이터베이스 접속 정보, 세션 토큰 등이 이에 해당됩니다. 만약 이런 크리덴셜이 외부에 노출된다면 어떻게 될까요?

생각만 해도 아찔한 일이 벌어질 수 있습니다. 우선 공격자가 크리덴셜을 탈취하게 되면, 일부 서비스를 조종할 수 있게 됩니다. 사이트를 마비 시킨다든가, 이용자를 속여 계정을 탈취 하는 일이 비일비재해지겠죠. 더 무서운 건 고객 정보 유출입니다. 오늘날 서비스는 개인정보를 비롯한 소중한 데이터를 대량으로 보관하고 있습니다. 공격자가 이런 데이터를 털어가면 어떻게 될까요? 유출자 개개인은 명의도용이나 금전적 피해를 입을 수 있고, 기업 입장에서는 법적 제재와 함께 신뢰를 잃어 막대한 손실을 떠안게 됩니다. 이는 기업의 존립을 위협할 수도 있는 치명적인 타격입니다.

즉, 크리덴셜은 마치 금고의 열쇠와도 같습니다. 금고 안에는 기업의 핵심 자산인 데이터들이 보관되어 있는데, 이 열쇠를 악의적인 공격자에게 넘기는 꼴이 되어서는 안 되는 것이죠. 따라서 열쇠를 잘 관리하듯, 크리덴셜 역시 외부 노출을 철저히 차단하고 접근을 엄격히 통제해야만 합니다. 특히 크리덴셜이 프론트엔드 코드에 포함되는 건 정말 위험천만한 일입니다. 브라우저라는 열린 공간에서 실행되는 프론트엔드 코드는 누구나 쉽게 내용을 들여다볼 수 있기 때문이죠. 따라서 어떤 이유로든 절대 민감한 크리덴셜이 프론트엔드에 남겨져서는 안 됩니다.

현실에서 우리는 계속해서 크리덴셜 유출을 보고하고 있습니다.

하지만, 올해 1월 Resend라는 스타트업이 바로 이런 일을 겪었습니다. 문제는 Resend의 프론트엔드 코드에 하드코딩된 데이터베이스 접속 정보가 고스란히 노출된 것입니다. 해커들은 클라이언트 소스 코드로 환경변수를 알아내고, 고객 데이터베이스에 마음대로 접근할 수 있게 되었죠. 그 결과 회사는 심각한 보안 사고를 겪게 되었습니다.

과연 이번 사례가 예외일까요? 안타깝게도 이런 사례는 비일비재합니다. 크리밋에서는 Ferret을 통해 한국 유명 웹사이트들의 프론트엔드 코드를 검증한 결과, 무려 70여개 사이트 중 14개가 크리덴셜이 노출되어 있었습니다. 한마디로 Resend와 비슷한 실수를 저지르고 있는 것이죠.

Credential Leak Example

구체적으로 어떤 문제들이 발견되었느냐하면, 가장 많이 발견된 것이 OAuth의 Client Secret 노출이었습니다. 이는 OAuth 인증에 사용되는 민감한 값으로, 유출시 서비스 전체가 위험에 빠질 수 있습니다. 그 다음으로 권한이 모두 열려있는 서드파티 액세스 토큰이 자주 발견되었습니다. 이 토큰들은 외부 API를 호출할 때 인증 수단으로 사용되는데, 만약 권한 설정을 제대로 하지 않으면 공격자가 토큰을 악용해 서비스의 핵심 기능들을 마음대로 조작할 수 있게 됩니다.

AWS Key Leaked

이외에도 AWS IAM Secret key 등 치명적인 크리덴셜들이 코드에 그대로 하드코드 된 것도 목격되었습니다. 하드코딩된 형태로 말이죠. 얼핏 보면 사소한 실수 같지만, 사실 엄청난 보안 위험을 내포하고 있습니다.

OIDC Token Leaked

이뿐만이 아닙니다. 일부 사이트에서는 빌드 서버에서 사용한 OIDC 액세스 토큰이 환경 변수로 남아 프론트엔드 빌드 파일에 고스란히 남겨져 있기도 했죠. 혹은 리포지토리에 실수로 커밋된 환경설정 파일에 크리덴셜이 고스란히 남아있는 경우도 있었습니다.

위 사례로 보아 많은 기업들이 프론트엔드 보안의 중요성을 간과하고 있다는 뜻이겠죠. 백엔드에 비해 프론트엔드는 덜 위험할 것이라는 막연한 안일함 때문인 것 같습니다. 하지만 프론트엔드에 내재된 보안 위험이 결코 가볍지 않다는 걸 이번 기회에 꼭 인지할 필요가 있습니다.

그렇다면 많은 개발자들이 프론트엔드 보안에 소홀한 이유는 무엇일까요? 사실 급한 마음에 귀찮은 환경변수 설정 대신 크리덴셜을 하드코딩하는 경우도 많습니다. 하지만, 무엇보다 최근 이런 사례가 점점 급증하고 있는 이유는 오늘날 프론트엔드 코드는 단순히 화면을 그리는 역할에 그치지 않고, 민감한 데이터를 직접 다루는 경우가 많아졌기 때문입니다.

개발 과정에서 어떻게 위와 같은 실수를 저지르는지 사례를 통해 알아보도록 하겠습니다. 먼저 Next.js 애플리케이션의 설정을 담당하는 next.config.js 파일을 살펴보겠습니다.

next js secret

위 코드는 개발 편의성을 추구하는 과정에서 흔히 발생할 수 있는 보안 이슈를 잘 보여주는 사례입니다. publicRuntimeConfig 옵션은 Next.js에서 제공하는 기능으로, 클라이언트 측에서 접근 가능한 환경변수를 설정할 수 있게 해줍니다. 주로 퍼블릭 API 키나 비즈니스 로직에 필요한 설정값 등을 다룰 때 유용하죠. 하지만 위 코드에서는 process.env를 spread 문법으로 통째로 할당하고 있습니다. 이는 서버의 모든 환경변수를 예외 없이 클라이언트에 노출시키겠다는 의미와 다름없습니다.

만약 process.env에 데이터베이스 접속 정보, 서드파티 서비스의 시크릿 키 등 민감한 크리덴셜이 포함되어 있다면 어떻게 될까요? 그대로 클라이언트 측 번들에 포함되어 모두의 접근이 가능해지고 맙니다. 브라우저 개발자 도구를 열면 process.env.DB_PASSWORD 같은 코드가 평문으로 노출될 것입니다. 이는 분명 개발 편의를 위한 방법이긴 하지만, 보안 측면에서는 상당히 위험한 코드라 할 수 있겠습니다.

next js ssr security issue

위 코드는 Next.js의 페이지 라우터에서 서버 사이드 렌더링(SSR) 도중에 하드코딩된 크리덴셜 키가 포함된 경우를 보여주고 있습니다. 일반적으로 Next.js는 서버 측 코드를 클라이언트 번들에 포함시키지 않습니다. 따라서 많은 개발자들이 편의상 시크릿 키를 하드코딩하곤 하죠. API 라우트에 비해 상대적으로 안전할 거라고 생각하기 때문입니다. 하지만 여기에는 함정이 도사리고 있습니다. 바로 소스맵 때문이죠.

예를 들어, Sentry 같은 에러 모니터링 솔루션을 도입하기 위해 소스맵을 업로드하는 경우가 있습니다. 소스맵이 있어야 난독화된 스택 트레이스를 원래 코드에 매핑할 수 있기 때문이죠.

그런데 문제는 이 소스맵에 서버 측 코드가 함께 포함될 수 있다는 점입니다. 다시 말해, API 라우트뿐만 아니라 getServerSideProps 등에 작성된 코드도 모두 노출되는 것이죠. 이는 상당히 위험한 상황입니다. Sentry는 매우 유용한 도구이지만, 동시에 공격 벡터가 될 수도 있다는 것을 보여줍니다.

Sentry Security issue

최근 Next.js와 React 생태계에는 혁신적인 변화의 바람이 불고 있습니다. 특히 Next.js 13에서 도입된 React Server Component(RSC)는 서버 측 렌더링과 클라이언트 측 렌더링의 경계를 허물며 개발 패러다임에 변혁을 가져왔습니다. 그리고 Next.js 14에서는 Server Action이라는 새로운 기능까지 추가되었죠.

이러한 기술들은 개발 생산성과 사용자 경험을 크게 향상시켜 줍니다. 하지만 동시에 보안 측면에서는 새로운 위험을 내포하고 있기도 합니다. 바로 서버 코드와 클라이언트 코드의 경계가 모호해지면서, 크리덴셜 노출의 가능성이 높아졌다는 점이죠. 아래 코드를 통해 이런 위험을 좀 더 살펴보겠습니다.

위 코드는 RSC를 활용한 간단한 폼 컴포넌트입니다. getEnv 함수를 통해 SECRET_TOKEN이라는 환경변수를 읽어오고, 이를 run 함수에 전달하여 서버에서 실행하는 로직이죠.

여기서 run 함수에는 "use server" 지시어가 추가되어 있습니다. 이는 해당 함수가 서버에서만 실행됨을 나타내는 것으로, 클라이언트에는 전송되지 않습니다. 따라서 얼핏 보면 SECRET_TOKEN이 클라이언트에 노출될 일은 없어 보입니다. 서버에서 안전하게 사용되고 있으니까요.

하지만 여기에는 큰 함정이 도사리고 있습니다. Next.js는 기본적으로 Server Component를 암호화하여 클라이언트에 전송합니다. 하지만 위 코드처럼 바인딩을 하게 된다면 Server Action을 사용하면 암호화가 자동으로 해제(opt-out)됩니다. 즉, SECRET_TOKEN의 값이 평문 그대로 클라이언트에 포함되어 버리는 것이죠. 브라우저의 개발자 도구를 열어보면 이를 확인할 수 있습니다.

next js frontend leaked

그래서 결론적으로, 우리는 무엇을 해야 할까요?

만약 여기에 중요한 크리덴셜 정보가 담겨있다면 말 그대로 대참사가 아닐 수 없겠죠. 공격자는 간단히 페이지 소스를 확인하는 것만으로도 비밀 키를 털어갈 수 있게 됩니다. 이는 RSC와 Server Action이 가진 양날의 검과도 같습니다. 개발 편의성은 높아졌지만, 그만큼 보안에는 더욱 신경을 써야 한다는 뜻이기도 하죠. 특히 서버 코드와 클라이언트 코드를 같은 파일에서 혼합하여 사용하는 패턴은 이런 실수를 유발하기 쉽습니다. 어떤 부분이 어디에서 실행되는지 한눈에 파악하기 어려워지기 때문이죠.

 

이처럼 프론트엔드 애플리케이션의 보안을 강화하기 위해서는 무엇보다 개발 프로세스 전반에 걸친 체계적인 접근이 필요합니다. 가장 기본적이면서도 중요한 것은 민감한 정보를 절대 프론트엔드 코드에 포함시키지 않는 것입니다. 아무리 편리하더라도 API 키나 시크릿 토큰 등의 크리덴셜이 JS 번들에 포함되어서는 안 되는 것이죠.

이를 위해서는 개발팀 전체가 시큐어 코딩 원칙을 이해하고 실천하는 것이 중요합니다. 정기적인 교육과 워크샵을 통해 안전한 코딩 습관을 익히고, 팀 내 코드 리뷰 문화를 통해 취약점을 사전에 발견해 나가야 할 것입니다. 체크리스트를 만들어 모든 코드 변경사항을 보안 관점에서 검토하는 것도 효과적이죠.

하지만, 아무리 완벽한 프로세스와 설계를 갖춘다 해도 휴먼 에러를 완벽히 차단하긴 어렵습니다.특히 Next.js의 Server Action이나 React Server Component 같은 최신 기술들은 서버와 클라이언트의 경계를 모호하게 만들어, 개발자들이 실수로 크리덴셜을 노출하기 쉬운 환경을 만들고 있습니다. 따라서 자동화된 검사 도구의 도움을 받는 것이 현명합니다. 크리밋의 Ferret은 소스코드 내 하드코딩된 크리덴셜을 탐지해 줄 뿐만 아니라, Notion, Jira와 같은 협업 도구에 산재한 API Key, 비밀번호 등을 찾아내 알려줍니다.

프론트엔드 개발에 있어 보안은 이제 선택이 아닌 필수입니다. 단순히 기술적 차원을 넘어, 비즈니스 지속가능성의 관점에서도 그렇습니다. 고객의 신뢰를 지키고 브랜드 가치를 높이기 위해서라도 할 수 있는 최선의 노력을 다해야만 합니다.

Resend 사태를 타산지석 삼아 프론트엔드 보안의 중요성을 깨닫고 적극적으로 나서야 할 때입니다. Ferret이 여러분의 빈틈없는 지킴이가 되어 드리겠습니다. 여러분의 코드가 안전해질수록, 모두가 더 나은 인터넷 세상을 만들어갈 수 있습니다. 지금 바로 크리밋에 문의 주시기 바랍니다.

크리밋은 사이버 세상을 안전하게 만들어 갑니다.

저희는 한국의 주요 사이트들의 자격증명 유출을 조사하고 많은 유출사례를 발견했습니다. 발견되어 위험하게 운영되고 있는 사이트들은 관리자들에게 통보 메일을 보냈습니다. 해당 게시물에 소개된 위협 외, 매우 위험한 유출 사례(알리바바 클라우드, AWS 클라우드의 관리자 Credential) 또한 발견하였습니다.

대부분의 취약한 사이트의 관리자는 몇 주간 알림을 읽지 않거나(약 20%) 알림에 응답하지 않았지만(약 35%) 크리밋이 지속적으로 (AWS 어카운트 매니저 팀과) 알렸고, 현재는 노출된 대부분의 위협을 해결하였습니다.

Cremit은 책임있는 공개(Responsible Disclosure), 사이버 세상의 위협을 줄이기 위한 노력을 하고 있습니다.

크리밋과 기술적인 토론을 하고 싶으신가요? 보안과 관련된 모든 것을 저희는 환영합니다. 미팅을 예약하고 크리밋 팀을 만나보세요.