문제의 발생
블록체인 기술을 활용한 NFT마켓 및 DeFi 를 서비스하는 회사에 프론트엔드 개발자로 근무하던 때 였다.
여느 때 처럼 프로젝트 개발을 하고 있던 중, 팀원 한 분이 운영환경 웹사이트 기능이 이상하게 동작한다고 공유해주셨다.
자세하게는 DeFi 기능 중에 token swap 과정에서 사용되는 블록체인 주솟값이 우리측에서 사용하는 주소가 아닌 다른 주소가 표기된다는 내용이었다.
이 상황에서 사용자가 해당 기능을 사용하게 될 경우, 사용자가 swap 하려는 token 및 트랜잭션 수수료가 다르게 표기된 주소로 전송되게 된다.
금융 계좌거래와는 달리, 블록체인에서 다른 주소로 token 을 전송하게 되면 해당 token 은 완전히 유실돼 버린다.
이는 금전적으로 피해가 발생하는 매우 심각한 상황이었다.
이슈를 공유받자마자 프론트엔드 팀은 현재 운영환경에 배포되어 있는 프로젝트 소스코드를 확인했다.
하지만 배포 된 Release 된 코드 상에는 블록체인 주솟값이 정상으로 적용되어 배포되었다.
이슈 원인 파악을 위해 재배포 전 현재 클라우드 서버에 배포되어있는 소스코드를 다운로드 받았다.
그리고는 운영 이슈로 인한재배포 공유를 하고 새로운 빌드 배포를 진행했다.
재배포가 완료되고 팀원 모두 이슈가 발생한 기능을 포함한 전기능에 대해 확인을 진행했다.
다행히도 재배포 후 이슈는 재현되지 않고 의도한 주솟값으로 되어있는 것을 확인했다.
이슈 원인 분석
이슈가 발생한 script 코드를 살펴보니 블록체인 주소가 모두 처음보는 주솟값으로 되어 있었다.
배포 과정에서 코드 변경이 이뤄지는 곳은 현재 구조에서 소스코드 빌드(컴파일) 과정 말고는 존재하지 않았다.
배포가 완료된 이후에는 script 코드 변경이 발생할 수 있는 사항으로는
- 공격자에 의해 HTML 에 의도하지 않은 script 가 로드되어 코드 변경 (XSS)
- 웹브라우저가 웹서버에서 script 파일을 받아오는 중간에 공격자의 웹서버를 거치면서 script 코드 변경
- 웹브라우저 콘솔을 사용한 script 실행
- 파일이 위치한 서버 해킹으로 인한 script 코드 변경
정도가 떠올랐다.
위 항목 중, 이미 서버에 배포된 script 파일에 변경이 있었으므로 4번으로 인해 발생한 이슈로 범위를 좁히고 대응책을 찾기 시작했다.
해결 방안
해당 프로젝트의 경우, 공격당한 파일들은 CDN 서버에 올려져 있었다.
'How protect script files from CDN server attacks?' 과 같이 CDN 서버가 해킹당했을 경우에 조치할 수 있는 방안에 대한 키워드로 검색을 하던 중, Subresource Integrity (SRI) 에 대해 찾을 수 있었다.
SRI 는 HTML 내에서 사용되는 하위 리소스 파일에 특정 알고리즘을 사용해 나온 해시(hash)값과 HTML script 또는 link 태그의 integrity 속성에 명시된 해시값이 일치하는지를 웹브라우저가 판단해 일치하는 경우에만 해당 리소스를 로드하는 방식이다.
(MDN - Subresource Integrity 바로가기)
당시 프로젝트는 create-next-app 로 생성한 Next.js 프레임워크 v12.x 를 사용 중이었으며, next build 시 webpack 이 사용되므로 webpack-sri 플러그인을 사용해 비교적 간단하게 적용할 수 있었다.
다만 integrity 속성을 지원하지 않는 웹브라우저가 있었으나, 서비스 운영방침에 의해 IE 및 2020년 이전 업데이트 버전의 브라우저라면 서비스 이용이 불가했기에 호환성 문제는 고려되지 않았다.
(Can i use 'integrity' 브라우저 호환성 바로가기)
Vite 환경에서 SRI 적용 예시
간단하게 Vite 환경에서 빌드 시 script 파일에 sri 를 적용하는 간단한 플러그인을 만들어 봤다.
Vite 의 plugin 공식문서를 보면 rollup plugin 인터페이스를 확장하여 사용한다고 나와있다.
이외에 플러그인 동작 로직은 공식문서 링크를 읽어보면 된다.
아마 대부분의 sri 적용 과정은 다음과 비슷할 것으로 생각된다.
1. 빌드된 script 혹은 css 파일을 불러온다.
2. 불러온 파일을 암호화 알고리즘을 적용해 hash 값을 생성한다.
3. 생성한 hash 값을 index.html 의 해당 script 혹은 link 태그의 integrity 속성값으로 설정한다.
writeBundle 함수에 로직이 담겨있다.
import fs from "fs";
import path from "path";
import crypto from "crypto";
const subresourceIntegrityPlugin = () => {
return {
name: "sri-plugin",
apply(config, { command }) {
// build 시에만 플러그인 적용
return command === "build" && !config.build.ssr;
},
writeBundle(outputOptions, bundle) {
try {
console.log("sri-plugin start");
for (const [fileName, file] of Object.entries(bundle)) {
if (file.type === "chunk" && fileName.endsWith(".js")) {
const jsOutDirFilePath = fileName;
const jsFileContent = fs.readFileSync(
path.resolve(outputOptions.dir, jsOutDirFilePath)
);
const hash = crypto
.createHash("sha384")
.update(jsFileContent)
.digest("base64");
const htmlFilePath = path.resolve(outputOptions.dir, "index.html");
const htmlContent = fs.readFileSync(htmlFilePath, "utf-8");
const scriptTagRegex = new RegExp(
`<script(.*?)src=["']\\/${jsOutDirFilePath}["'](.*?)><\\/script>`,
"g"
);
const newHtmlContent = htmlContent.replace(
scriptTagRegex,
`<script$1src="${jsOutDirFilePath}"$2 integrity="sha384-${hash}" crossorigin="anonymous"></script>`
);
fs.writeFileSync(htmlFilePath, newHtmlContent, "utf-8");
}
}
console.log("sri-plugin end");
} catch (error) {
console.error("sri-plugin error :", error);
}
},
};
};
export default subresourceIntegrityPlugin;
플러그인을 적용하고 프로젝트를 빌드 및 실행하면 script 태그에 integrity 속성이 추가된 것을 확인할 수있다.
만약 CDN 이 공격당해 `assets/index-0Lroe-T8.js` 파일 내용이 변경되면 hash 또한 변경되므로 해당 script 파일은 브라우저에 의해 차단된다.
'새롭게 알게된 것들' 카테고리의 다른 글
Client-side rendering(CSR) 구조 웹사이트 검색엔진최적화(SEO) 적용 경험 (0) | 2024.08.05 |
---|---|
window.history.pushState 로 추가한 뒤, 브라우저 뒤로가기 클릭 시 브라우저마다 다르게 동작하는 증상 (0) | 2024.07.25 |
Typescript Parcel Tilde(~)경로 적용 시, Resolver Error 해결 (삽질기) (2) | 2019.12.05 |
[express] express request.body undefined 문제 해결 (23) | 2018.03.30 |
CentOS 7 설치 삽질기 (feat. boot usb) (3) | 2017.03.02 |