/pair approve가 Scope Guard를 우회할 때
Source: Dev.to
설정
OpenClaw의 디바이스 페어링 시스템은 전화기, 태블릿 및 기타 “노드”를 게이트웨이에 연결할 수 있게 해줍니다. 디바이스가 페어링될 때 특정 스코프가 포함된 토큰을 받습니다 — 스코프를 권한 수준이라고 생각하면 됩니다. operator.pairing은 디바이스 연결을 관리할 수 있게 해주고, operator.admin은… 모든 것을 할 수 있게 해줍니다.
신뢰 모델은 명확합니다: admin‑스코프를 가진 연산자만 admin 스코프를 부여하는 페어링 요청을 승인할 수 있어야 합니다.
이것은 핵심 approveDevicePairing 함수에서 강제됩니다. 이 함수는 선택적인 callerScopes 매개변수를 받습니다. 매개변수가 있으면 다음을 확인합니다: 호출자가 충분한 스코프를 가지고 있는가? 없으면 거부합니다. 좋은 설계이며, 이에 대한 테스트도 존재합니다.
우회
device-pair 플러그인은 /pair approve 슬래시 명령을 노출합니다:
if (action === "approve") {
// 대략적인 검사: 호출자가 *어떤* 페어링 관련 스코프를 가지고 있는가?
if (gatewayClientScopes &&
!gatewayClientScopes.includes("operator.pairing") &&
!gatewayClientScopes.includes("operator.admin")) {
return { text: "Requires operator.pairing" };
}
// callerScopes를 전달하지 않고 승인
const approved = await approveDevicePairing(pending.requestId);
}
슬래시 명령은 “어떤 페어링 스코프를 가지고 있나요?”를 확인하지만 approveDevicePairing()을 callerScopes를 전달하지 않고 호출합니다. 핵심 함수는 callerScopes가 존재할 때만 스코프 가드를 적용합니다.
따라서 operator.pairing만 가진 연산자도 operator.admin을 요구하는 보류 중인 요청을 /pair approve 할 수 있게 됩니다. 권한 상승이 완성됩니다.
왜 이런 패턴이 계속 발생하는가
이것은 이중 경로 권한 부여 격차입니다:
- 경로 A (RPC/API): 신중하게 설계되어 모든 인증 컨텍스트를 전달합니다.
- 경로 B (편의 레이어): 나중에 친절한 래퍼로 만들어집니다. 동일한 핵심 함수를 호출하지만 중요한 매개변수를 전달하는 것을 잊습니다.
핵심 함수가 callerScopes를 선택적으로 만들었습니다. 그 선택성이 취약점이 되었습니다.
- 핵심 함수는 “
callerScopes가 없으면 호출자를 신뢰한다”는 가정을 합니다. - 플러그인은 “대략적인 스코프 검사가 통과하면 괜찮다”는 가정을 합니다.
- 각각의 가정은 별개로는 틀리지 않지만, 함께 작동하면 CVSS 9.9 수준의 문제를 초래합니다.
해결 방법
여덟 줄이면 됩니다. callerScopes를 전달하면 됩니다. 테스트도 하나 추가합니다.
에이전트 제작자를 위한 교훈
- 선택적인 보안 매개변수는 위험합니다.
callerScopes가 필수였더라면 플러그인 작성자는 무엇을 전달해야 할지 강제되었을 것입니다. - 특권 행동에 이르는 모든 경로는 동일한 검사를 받아야 합니다. RPC, 슬래시 명령, API 엔드포인트, 크론 핸들러 — 각각을 독립적으로 테스트하세요.
- 편의 레이어가 인증 버그가 숨는 곳입니다. 핵심 인프라가 견고한 권한 부여를 구현했지만, 플러그인은 좋은 UX 래퍼를 만들었습니다. 아무도 래퍼가 보안 속성을 유지하는지 확인하지 않았습니다.
- 페어링 및 프로비저닝은 신뢰를 부여하는 작업입니다. 인증만큼이나 면밀히 검토해야 합니다.