3004 단어
15 분
Polyfill.io로 알아보는 시큐어 코딩과 의존성 관리

시큐어 코딩이라고 하면 보통 SQL Injection, XSS, 비밀번호 해시 처리 같은 전통적인 보안 개념을 먼저 떠올린다. 물론 이런 내용도 중요하다. 하지만 실제 개발에서는 내가 직접 작성한 코드뿐만 아니라 프로젝트가 외부에서 불러오는 스크립트와 라이브러리도 보안에 큰 영향을 준다. 구버전 브라우저 호환성을 위해 널리 쓰이던 Polyfill.io는 도메인 소유권이 바뀐 뒤 악성 스크립트가 삽입되는 공급망 공격이 발생했고 CVE-2024-38526으로 등록되었다. 따라서 필자는 이번 글에서 Polyfill.io 사례를 중심으로 시큐어 코딩을 왜 단순한 코드 작성 습관이 아니라 전체 개발 과정의 보안 관리로 봐야 하는지 설명하도록 하겠다.


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

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

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

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

2. 입력과 출력에 대한 기본 방어#

2.1 입력값 검증#

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

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

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: "올바른 나이를 입력하세요."
});
}

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

2.2 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을 작성하는 부분에서는 항상 확인해야 한다.

2.3 React에서의 XSS 방지#

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

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

하지만 HTML을 직접 삽입하는 방식은 조심해야 한다. dangerouslySetInnerHTML은 이름 그대로 위험할 수 있는 기능이며, 사용자가 입력한 HTML을 그대로 넣으면 XSS 취약점이 생긴다.

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

정말 HTML 삽입이 필요한 상황이라면 정제 과정을 거쳐야 한다.

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

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

3. 외부 의존성과 공급망 보안#

3.1 Polyfill.io가 보여준 문제#

Polyfill.io 사례는 시큐어 코딩을 외부 리소스 영역까지 넓게 봐야 한다는 점을 보여준다. 2024년 2월 한 업체(Funnull)가 polyfill.io 도메인을 인수한 뒤, 이 CDN은 자바스크립트 리소스를 요청한 사용자의 환경을 분석해 주로 모바일 사용자를 도박·피싱 사이트로 리다이렉트하는 악성 코드를 배포하기 시작했다. 이 코드는 관리자나 분석 도구가 감지되면 동작하지 않도록 설계되어 한동안 탐지를 피했고, 그 결과 10만 개가 넘는 웹사이트가 영향을 받은 것으로 알려졌다. 중요한 것은 이 취약점이 우리가 직접 작성한 백엔드나 프론트엔드 로직의 결함이 아니라, 신뢰하고 한 줄 써넣었던 외부 스크립트 태그 그 자체에서 비롯된 문제였다는 점이다.

보안 문제가 꼭 내부 시스템 안에서만 발생하는 것은 아니다. 프로젝트에서 무심코 불러오는 타사 CDN, 광고 스크립트, 분석 툴 같은 외부 파일도 서비스 전체를 오염시키는 통로가 될 수 있다.

과거에는 프론트엔드가 단순히 브라우저 화면에 데이터를 뿌려주는 역할에 그쳤지만, 이제는 사용자 세션 토큰을 다루고 중요한 로직을 수행하는 주체가 되었다. 따라서 외부 도메인에서 스크립트를 동적으로 로드할 때는 항상 공급망 공격의 대상이 될 수 있음을 인지해야 한다.

3.2 의존성 관리#

시큐어 코딩은 내가 작성한 코드만 관리하는 것이 아니다. 외부 라이브러리와 패키지, 원격 스크립트의 상태를 꾸준히 확인하는 것도 보안의 일부다. 실제로 Polyfill.io 사건 직후 Cloudflare와 Fastly는 안전한 미러 CDN을 제공했고, Polyfill의 원 개발자 역시 서비스 사용 중단을 권고했다.

3.2.1 하위 리소스 무결성(SRI)#

외부 스크립트를 반드시 불러와야 한다면, 하위 리소스 무결성(Subresource Integrity, SRI) 속성을 사용해 파일의 변조 여부를 브라우저가 검증하도록 해야 한다. integrity에 명시한 해시와 실제로 내려받은 파일의 해시가 다르면 브라우저는 스크립트 실행을 거부한다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"
integrity="sha512-WFN0484M1isFh5KKTIaEDFM68QAttlhcbfTbjk6mfnfvygEGdpHJY6mskvufbIQKHdoIAa5bfnw9UpCc3MStWg=="
crossorigin="anonymous"></script>

3.2.2 패키지 취약점 점검#

프로젝트 내부의 패키지 취약점은 다음과 같이 확인할 수 있다.

Terminal window
npm audit

또는 package.json에서 사용하지 않거나 신뢰할 수 없는 외부 저장소 주소가 섞여 있는지 직접 확인할 수도 있다.

{
"dependencies": {
"lodash": "4.17.21",
"dompurify": "3.0.5"
}
}

보안 이슈가 발생한 도메인의 스크립트는 즉시 제거하거나 신뢰할 수 있는 대안(오픈소스 라이브러리를 직접 번들링하거나 대형 기업이 운영하는 대체 CDN 등)으로 교체해야 한다. 교체 후에는 기존 기능이 깨질 수 있으므로 테스트 환경에서 먼저 확인하는 과정이 필요하다. 하지만 알려진 취약점을 계속 방치하는 것이 훨씬 더 위험하다.

4. 인증·인가와 민감 정보 처리#

4.1 인증과 인가#

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

구분인증(Authentication)인가(Authorization)
핵심 질문당신은 누구인가그 작업을 할 권한이 있는가
수행 시점로그인 시 신원 확인작업 수행 직전
실패 응답401 Unauthorized403 Forbidden

예를 들어 게시글 삭제 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: "삭제 완료"
});
});

이런 권한 검사는 실제 서비스에서 자주 빠지는 부분이기 때문에 주의해야 한다.

4.2 비밀번호 저장#

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

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: "로그인 정보가 올바르지 않습니다."
});
}

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

4.3 에러 메시지와 로그 처리#

개발 중에는 자세한 에러 메시지가 도움이 되지만, 운영 환경에서 내부 에러 정보가 그대로 노출되면 서버 경로나 구조가 공격자에게 힌트로 작용한다.

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: "요청을 처리하는 중 문제가 발생했습니다."
});
});

다만 로그에도 비밀번호, 인증 토큰, 개인정보 같은 민감한 정보가 남지 않도록 주의해야 한다.


오늘은 Polyfill.io 사례를 중심으로 입력·출력 방어와 외부 의존성 관리, 그리고 인증·인가와 민감 정보 처리에 대해 알아보았다.

Polyfill.io로 알아보는 시큐어 코딩과 의존성 관리
https://blog.paisl.cloud/posts/009/
저자
PAISL
발행일
2026-06-05
콘텐츠 라이선스
CC BY-NC-SA 4.0