2918 단어
15 분
React2Shell로 알아보는 시큐어 코딩과 의존성 관리

시큐어 코딩이라고 하면 보통 SQL Injection, XSS, 비밀번호 해시 처리 같은 전통적인 보안 개념을 먼저 떠올린다. 물론 이런 내용도 중요하다. 하지만 실제 개발에서는 내가 직접 작성한 코드뿐만 아니라, 프로젝트에서 사용하는 프레임워크와 라이브러리도 보안에 큰 영향을 준다.

React2Shell은 React Server Components와 관련된 취약점으로 알려졌고, CVE-2025-55182로 등록되었다. React 공식 블로그에서는 이 취약점이 CVSS 10.0으로 평가되었고, React 19.0, 19.1.0, 19.1.1, 19.2.0 버전의 react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack 패키지에 영향을 준다고 안내했다. NVD 설명에 따르면 이 취약점은 Server Function 엔드포인트로 들어오는 HTTP 요청 payload를 안전하지 않게 역직렬화하는 문제와 관련되어 있으며, 사전 인증 원격 코드 실행으로 이어질 수 있다.

필자는 이번 글에서 React2Shell 사례를 중심으로 시큐어 코딩을 왜 단순한 코드 작성 습관이 아니라 전체 개발 과정의 보안 관리로 봐야 하는지 설명하도록 하겠다.


1. 시큐어 코딩을 다시 봐야 하는 이유#

처음 개발을 공부할 때는 보통 기능 구현에 집중하게 된다. 로그인 기능이 되는지, 게시글이 저장되는지, API가 정상적으로 응답하는지 같은 부분을 먼저 확인한다. 기능이 정상적으로 돌아가면 코드가 어느 정도 완성되었다고 생각하기 쉽다.

하지만 실제 서비스에서는 “돌아가는 코드”와 “안전한 코드”가 다르다. 사용자가 예상하지 못한 값을 입력했을 때 문제가 생길 수 있고, 권한 검사를 빼먹으면 다른 사람의 데이터를 수정할 수도 있다. 또한 React2Shell처럼 내가 직접 작성한 코드가 아니더라도, 사용하는 프레임워크 내부 취약점 때문에 서비스가 위험해질 수 있다.

따라서 시큐어 코딩은 단순히 몇 가지 보안 문법을 외우는 것이 아니라, 코드를 작성하고 배포하고 유지보수하는 전체 과정에서 보안을 고려하는 습관이라고 볼 수 있다.

2. 입력값 검증#

시큐어 코딩에서 가장 기본이 되는 것은 사용자의 입력값을 그대로 믿지 않는 것이다. 회원가입, 로그인, 댓글, 검색창, 파일 업로드처럼 사용자가 값을 넣는 부분은 모두 공격 지점이 될 수 있다.

예를 들어 나이를 입력받는 코드가 있다고 해보자.

const age = req.body.age;
if (age >= 14) {
console.log("가입 가능");
}

위 코드는 사용자가 항상 정상적인 숫자를 보낸다고 가정한다. 하지만 실제 서비스에서는 문자열이나 비정상적으로 큰 값이 들어올 수 있다. 그래서 서버에서는 타입과 범위를 다시 확인해야 한다.

const age = Number(req.body.age);
if (!Number.isInteger(age) || age < 0 || age > 120) {
return res.status(400).json({
message: "올바른 나이를 입력하세요."
});
}

프론트엔드에서 입력값 검사를 하더라도 서버 검증은 반드시 필요하다. 브라우저에서 하는 검사는 사용자가 우회할 수 있기 때문이다.

3. SQL Injection 방지#

SQL Injection은 사용자의 입력값을 이용해 SQL 쿼리를 조작하는 취약점이다. 오래된 공격 방식이지만, 기본을 지키지 않으면 여전히 발생할 수 있다.

아래와 같은 코드는 위험하다.

const query =
"SELECT * FROM users WHERE id = '" +
userId +
"' AND password = '" +
password +
"'";

사용자의 입력값이 SQL 문장에 그대로 들어가기 때문에 공격자가 특수한 입력값을 넣으면 쿼리의 의미가 바뀔 수 있다. 따라서 SQL을 문자열로 직접 조립하는 방식은 피하는 것이 좋다.

더 안전한 방식은 파라미터 바인딩을 사용하는 것이다.

const query = "SELECT * FROM users WHERE id = ? AND password = ?";
db.execute(query, [userId, password]);

이렇게 작성하면 사용자가 입력한 값은 SQL 명령어가 아니라 단순한 데이터로 처리된다. ORM을 사용하더라도 직접 SQL을 작성하는 부분에서는 항상 확인해야 한다.

4. React에서의 XSS 주의#

React는 JSX 안에서 문자열을 출력할 때 기본적으로 이스케이프 처리를 해준다. 그래서 일반적인 문자열 출력은 비교적 안전하다.

function Comment({ content }) {
return <p>{content}</p>;
}

하지만 HTML을 직접 삽입하는 방식은 조심해야 한다.

function Comment({ content }) {
return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

dangerouslySetInnerHTML은 이름 그대로 위험할 수 있는 기능이다. 사용자가 입력한 HTML을 그대로 넣으면 XSS 취약점이 생길 수 있다.

정말 HTML 삽입이 필요한 상황이라면 정제 과정을 거치는 것이 좋다.

import DOMPurify from "dompurify";
function Comment({ content }) {
const safeContent = DOMPurify.sanitize(content);
return <div dangerouslySetInnerHTML={{ __html: safeContent }} />;
}

React를 사용한다고 해서 XSS 위험이 완전히 사라지는 것은 아니다. 프레임워크가 기본적인 방어를 도와주더라도, 개발자가 위험한 기능을 잘못 사용하면 취약점은 충분히 발생할 수 있다.

5. React2Shell이 보여준 문제#

React2Shell 사례는 시큐어 코딩을 더 넓게 봐야 한다는 점을 보여준다. 이 취약점은 React Server Components와 관련된 문제였고, 특정 React 19 버전의 서버 컴포넌트 관련 패키지에 영향을 주었다.

중요한 점은 보안 문제가 꼭 내가 직접 작성한 코드에서만 발생하는 것이 아니라는 것이다. 프로젝트에서 사용하는 프레임워크나 라이브러리 내부 취약점도 서비스 전체에 영향을 줄 수 있다.

요즘 React나 Next.js 같은 프레임워크는 단순히 화면만 그리는 도구가 아니다. 서버 사이드 렌더링, 서버 컴포넌트, API 라우트처럼 서버와 연결되는 기능이 많다. 따라서 프론트엔드 개발자도 서버 보안과 의존성 관리에 관심을 가져야 한다.

6. 의존성 관리#

시큐어 코딩은 내가 작성한 코드만 관리하는 것이 아니다. 외부 라이브러리와 패키지를 꾸준히 확인하는 것도 보안의 일부이다.

프로젝트에서 사용 중인 React, React DOM, Next.js 버전은 다음과 같이 확인할 수 있다.

Terminal window
npm list react react-dom next

또는 package.json 파일에서 직접 확인할 수도 있다.

{
"dependencies": {
"react": "19.2.0",
"react-dom": "19.2.0",
"next": "15.0.0"
}
}

취약한 버전을 사용하고 있다면 패치된 버전으로 업데이트해야 한다.

Terminal window
npm update
npm audit

Next.js를 사용하는 경우에는 Next.js 버전도 함께 확인하는 것이 좋다.

Terminal window
npm list next
npm install next@latest

물론 무조건 최신 버전으로 올리는 것이 항상 정답은 아니다. 업데이트 후 기존 기능이 깨질 수 있으므로 테스트 환경에서 먼저 확인하는 과정이 필요하다. 하지만 보안 취약점이 알려진 버전을 계속 방치하는 것은 더 위험하다.

7. 인증과 인가#

인증과 인가는 비슷해 보이지만 다른 개념이다. 인증은 사용자가 누구인지 확인하는 과정이고, 인가는 그 사용자가 특정 작업을 할 권한이 있는지 확인하는 과정이다.

예를 들어 게시글 삭제 API가 있다고 해보자.

app.delete("/posts/:id", async (req, res) => {
await deletePost(req.params.id);
res.json({
message: "삭제 완료"
});
});

위 코드는 누가 요청했는지 확인하지 않고 게시글을 삭제한다. 실제 서비스에서는 매우 위험한 코드이다.

조금 더 안전하게 작성하면 다음과 같다.

app.delete("/posts/:id", async (req, res) => {
const post = await findPostById(req.params.id);
if (!req.user) {
return res.status(401).json({
message: "로그인이 필요합니다."
});
}
if (post.authorId !== req.user.id && req.user.role !== "admin") {
return res.status(403).json({
message: "삭제 권한이 없습니다."
});
}
await deletePost(req.params.id);
res.json({
message: "삭제 완료"
});
});

로그인 여부만 확인하는 것이 아니라, 해당 사용자가 정말 그 게시글을 삭제할 수 있는 사람인지 확인해야 한다. 이런 권한 검사는 실제 서비스에서 자주 빠질 수 있는 부분이기 때문에 주의해야 한다.

8. 비밀번호 저장#

비밀번호를 데이터베이스에 그대로 저장하는 것은 매우 위험하다.

await db.execute("INSERT INTO users (email, password) VALUES (?, ?)", [
email,
password
]);

데이터베이스가 유출되면 사용자의 비밀번호가 그대로 노출된다. 따라서 비밀번호는 해시 처리해서 저장해야 한다.

import bcrypt from "bcrypt";
const hashedPassword = await bcrypt.hash(password, 10);
await db.execute("INSERT INTO users (email, password) VALUES (?, ?)", [
email,
hashedPassword
]);

로그인할 때는 사용자가 입력한 비밀번호와 저장된 해시값을 비교한다.

const isValid = await bcrypt.compare(inputPassword, user.password);
if (!isValid) {
return res.status(401).json({
message: "로그인 정보가 올바르지 않습니다."
});
}

비밀번호 저장 방식은 기본적인 내용처럼 보이지만, 실수하면 피해가 매우 크다. 그래서 회원 기능을 만들 때 반드시 확인해야 하는 부분이다.

9. 에러 메시지 처리#

개발 중에는 자세한 에러 메시지가 도움이 된다. 하지만 운영 환경에서 내부 에러 정보가 그대로 사용자에게 노출되면 공격자에게 힌트를 줄 수 있다.

app.use((err, req, res, next) => {
res.status(500).json({
message: err.message,
stack: err.stack
});
});

위 코드는 서버 내부 경로나 구조가 노출될 수 있다. 운영 환경에서는 사용자에게 일반적인 메시지만 보여주는 것이 좋다.

app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
message: "요청을 처리하는 중 문제가 발생했습니다."
});
});

로그는 개발자가 문제를 해결하기 위해 필요하지만, 로그에도 비밀번호, 인증 토큰, 개인정보 같은 민감한 정보가 남지 않도록 주의해야 한다.


오늘은 React2Shell 사례를 중심으로 시큐어 코딩과 의존성 관리에 대해서 알아보았다. 시큐어 코딩은 단순히 SQL Injection이나 XSS만 막는 것이 아니라, 입력값 검증, 권한 확인, 비밀번호 저장, 에러 처리, 패키지 업데이트까지 포함하는 개발 습관이다.

React2Shell 사례처럼 보안 문제는 내가 직접 작성한 코드가 아닌 프레임워크와 라이브러리에서도 발생할 수 있다. 따라서 개발자는 기능 구현뿐만 아니라 자신이 사용하는 기술의 버전과 보안 이슈도 꾸준히 확인해야 한다.

다음 글에서는 실제 프로젝트에서 적용할 수 있는 보안 체크리스트와 코드 리뷰 관점의 시큐어 코딩 방법에 대해서 다루도록 하겠다.

참고 자료

  • React 공식 블로그, Critical Security Vulnerability in React Server Components
  • NVD, CVE-2025-55182
  • Vercel Knowledge Base, React2Shell Security Bulletin
React2Shell로 알아보는 시큐어 코딩과 의존성 관리
https://blog.paisl.cloud/posts/003/
저자
PAISL
발행일
2026-04-24
콘텐츠 라이선스
CC BY-NC-SA 4.0