
Seal Security 예제 — 취약한 npm 앱(EJS CVE-2022-29078)이 Sealed 버전으로 수정됨; GitHub Actions + Jenkins 연동
알려진 CVE를 Seal Security가 취약한 의존성을 sealed(백포팅된, drop‑in) 버전으로 교체하여 해결하는 방법을 처음부터 끝까지 보여 주기 위한, 최소화되고 의도적으로 취약하게 만든 Node.js/Express 애플리케이션입니다. 선언된 버전 범위나 코드는 전혀 변경하지 않습니다.
CI/CD에서 Seal CLI를 위한 처음부터 끝까지의 스모크 테스트로 설계되었습니다. 앱을 실행하고, 실제 익스플로잇을 트리거하고, Seal을 실행한 뒤, 동일한 익스플로잇이 차단되는 것을 확인하는 방식입니다.
| 생태계 | JavaScript / npm |
| 취약한 패키지 | [email protected](2.7.4로 해석됨) |
| CVE | CVE‑2022‑29078 — EJS 서버 사이드 템플릿 인젝션 → 원격 코드 실행(CVSS 9.8) |
| Sealed(수정된) 버전 | Seal의 npm 레지스트리에서 제공하는 ejs 2.7.4-sp1 |
| 통합 | 하나의 빌드 단계로 추가되는 Seal CLI — GitHub Actions 및 Jenkins 모두에 대해 설명 |
| 통합 | 하나의 빌드 단계로 추가되는 Seal CLI — GitHub Actions 및 Jenkins 모두에 대해 설명 |
| --- | --- |
이 앱에는 잘 알려진 다른 취약한 의존성(lodash 4.17.5, json5 0.5.1, got 6.7.1)도 포함되어 있으며, Seal은 이 각각도 sealed 빌드로 수정합니다.
이 앱은 URL 쿼리 문자열 전체를 그대로 EJS 렌더 호출에 펼쳐 넣습니다.
const data = { name: 'World', ...req.query };
res.render('page', data, ...)
EJS는 settings['view options'] 객체를 허용하며, 이 객체의 outputFunctionName 값은 소독(검증)되지 않은 채 컴파일된 템플릿 함수 본문에 작성됩니다. 따라서 공격자는 Node.js 프로세스의 권한으로 서버에서 실행되는 임의의 JavaScript를 주입할 수 있습니다.
일반 요청
/?name=alice
Hello alice!를 렌더링합니다.
익스플로잇 요청
/?name=Hacker&settings[view%20options][outputFunctionName]=x;setTimeout(function()%7Bprocess.exit(1)%7D,3000);s
서버는 setTimeout(function(){ process.exit(1) }, 3000)를 실행합니다. 페이지는 먼저 로드되면서 RCE가 성공했음을 분명히 표시합니다. 몇 초 후 다시 로드하면 ERR_CONNECTION_REFUSED가 표시됩니다. 주입된 코드가 서버를 종료시킨 것이며, 이는 임의 코드가 실행되었음을 증명합니다.
3초 지연은 의도적입니다. 프로세스가 종료되기 전에 응답이 브라우저에 도달하게 하여, "exploit succeeded" 페이지를 본 다음 탭이 멈추는 대신 깔끔하게 크래시되는 것을 보기 위함입니다.
.
├── index.js # the vulnerable Express app
├── views/ # EJS templates
├── package.json / package-lock.json
├── Jenkinsfile # example Jenkins (Groovy) pipeline with the Seal stage
└── .github/workflows/
├── build-and-run.yml # build + expose the app for browser testing
└── seal-security.yml # run Seal remediation, then start the app
Seal은 SaaS, Seal 호스팅 방식입니다. 사용자 환경에는 아무것도 설치되지 않으며, 모든 트래픽은 TCP 443의 아웃바운드 HTTPS뿐입니다. 수정 작업을 실행하려면 다음이 필요합니다.
| 시크릿 / 자격 증명 | 용도 | 위치 |
|---|---|---|
Settings → Secrets and variables → Actions(GitHub) 또는 Manage Jenkins → Credentials(Jenkins)에서 설정하세요. 토큰을 저장소에 커밋하지 마세요.
아웃바운드 443용 Seal 호스트를 허용 목록에 추가하세요: app.sealsecurity.io, authorization.sealsecurity.io, cli.sealsecurity.io, 그리고 sealed npm 패키지용 npm.sealsecurity.io. CLI 바이너리는 github.com / objects.githubusercontent.com에서 다운로드됩니다.
npm install
npm start # → http://localhost:3001
http://localhost:3001/?name=alice를 엽니다(정상 동작). 그다음 위의 익스플로잇 URL을 열면 서버가 크래시됩니다.
Seal CLI는 npm install 이후, 패키징 이전에 한 단계만 추가되어 실행됩니다. 해석된 의존성을 스캔하고 취약한 의존성을 sealed 버전으로 재작성하며, remote 수정 모드를 사용합니다(정책은 Seal UI에서 중앙 관리됩니다).
이 방식은 seal-community/cli-action을 사용합니다:
- uses: seal-community/cli-action@latest
with:
mode: fix
fix_mode: remote
token: ${{ secrets.SEAL_TOKEN }}
target: package-lock.json # the lock file for this ecosystem
Actions → “Seal Security Remediation” → Run workflow로 실행하세요. .github/workflows/seal-security.yml을 참조하세요.
설치 후, 패키징 전에 추가되는 단일 스테이지입니다. Jenkinsfile을 참조하세요:
stage('Seal') {
steps {
sh '''
curl -fsSL https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest -o seal
chmod +x seal
./seal fix --mode remote "$SEAL_MANIFEST" # SEAL_MANIFEST=package-lock.json
'''
}
}
SEAL_TOKEN은 seal-token Jenkins 자격 증명에서 가져옵니다. SEAL_PROJECT는 자신의 Seal 프로젝트 ID로 설정하세요.
seal fix 실행 후, 취약한 의존성은 Seal 레지스트리의 sealed 빌드로 해석됩니다. package.json의 버전 범위는 그대로 유지됩니다.
sealed 버전은 보안 수정 사항이 백포팅된 동일한 패키지이므로, 코드 변경이나 메이저 버전 업그레이드 없이 그대로 대체할 수 있는(drop-in) 버전입니다.
수정된 앱에 익스플로잇 URL을 다시 실행하세요. 주입은 더 이상 실행되지 않습니다. sealed ejs가 악성 outputFunctionName을 거부하고, 앱은 페이로드를 실행하는 대신 **“Invalid parameter”**로 응답합니다. 서버는 계속 작동합니다.
seal fix가 특정 매니페스트/잠금 파일을 가리키게 하세요. npm의 경우 package-lock.json입니다. 매니페스트가 여러 개인 저장소라면 매니페스트마다 seal fix를 한 번씩 실행하세요.이것이 통합의 전부입니다. 단일 스테이지, 아웃바운드 전용, 애플리케이션 코드 변경 없음.
| Seal CLI 인증 |
GitHub Actions 시크릿 SEAL_TOKEN / Jenkins "Secret text" 자격 증명 seal-token |
| ngrok 토큰 (선택 사항) | 실행 중인 앱을 테스트용 브라우저에 노출 | GitHub Actions 시크릿 NGROK_TOKEN |
| 의존성 | 이전 | 이후(sealed) |
|---|
| ejs | 2.7.4 | 2.7.4‑sp1 |
| lodash | 4.17.5 | 4.17.5‑sp1 |
| json5 | 0.5.1 | 0.5.1‑sp1 |
| got | 6.7.1 | 6.7.1‑sp1 |