flawopen.com/참고 자료/PR 봇 보안
AI PR 리뷰 봇: Prompt Injection SSRF 방지
신뢰할 수 없는 pull request 텍스트가 AI 도구에 미치는 영향과 봇 권한 및 네트워크 접근을 제한하는 방법을 설명합니다.
쉬운 설명: 사내 네트워크 통행증을 가진 리뷰 담당자
Pull Request는 리뷰 팀에 전달된 소포와 같습니다. 내용은 유용할 수 있지만 회사 정책이 되는 것은 아닙니다. AI 리뷰 봇이 사설 네트워크에서 임의 URL을 가져올 수도 있다면, 오해를 유도하는 지시가 접근 대상을 바꿀 수 있습니다. 위험은 도구와 네트워크 권한에 달려 있습니다. 적대적인 텍스트를 읽는 것만으로 네트워크 접근 권한이 생기지는 않습니다.
핵심 개념과 주요 용어
- 신뢰할 수 없는 입력:
- 기여자가 제출한 Pull Request 제목, 설명, 댓글, diff 및 링크된 콘텐츠.
- 간접 Prompt Injection:
- 신뢰할 수 없는 텍스트가 모델 컨텍스트에 포함되어 에이전트에 영향을 주는 현상.
- SSRF 경계:
- 서버 측 가져오기 도구가 기여자가 직접 접근할 수 없는 목적지에 도달하는 것.
- 영향 조건:
- 접근 가능한 내부 서비스, 사용 가능한 자격 증명, 부족한 권한 또는 송신 제어가 함께 있어야 합니다.
작동 방식과 근본 원인
리뷰 봇은 기여자가 제어하는 텍스트와 신뢰할 수 있는 리뷰 지침을 모델에 함께 전달할 수 있습니다. 모델은 어느 쪽이든 근거로 도구 호출을 제안할 수 있습니다. 프롬프트와 구분자는 네트워크 접근을 허가하지 않습니다. 애플리케이션 코드가 모델이 고른 목적지를 받아들이고 네트워크 도구가 그곳에 도달할 때만 SSRF가 가능합니다. 클라우드 메타데이터나 자격 증명 접근은 추가적인 조건부 영향이지 Prompt Injection의 자동 결과가 아닙니다.
조건부 공격 경로
기여자가 PR 콘텐츠를 제어
외부 기여자는 제목, 설명, 댓글, 변경된 텍스트, 링크된 자료에 영향을 줄 수 있습니다.
에이전트가 콘텐츠를 읽음
서비스가 일부 신뢰할 수 없는 필드를 모델 컨텍스트에 넣으면 응답이나 도구 제안에 영향을 줄 수 있습니다.
도구 능력이 도달 범위를 결정
에이전트가 임의 URL을 가져올 수 있으면 runner가 네트워크에서 허용하는 대상에 접속할 수 있습니다. 해당 도구가 없다면 이 SSRF 경로는 없습니다.
영향은 배포 환경에 좌우됨
내부 데이터나 자격 증명은 접근 가능한 서비스, 메타데이터 설정, 권한, 출력 제어가 허용할 때만 노출됩니다.
코드: 무검증 호출과 권한이 확인된 읽기
# INERT DEMO: model-selected URL reaches a mock tool without policy checks
from dataclasses import dataclass
@dataclass
class ToolCall:
name: str
arguments: dict[str, str]
class MockFetch:
def __init__(self):
self.calls = []
def __call__(self, url: str) -> str:
self.calls.append(url)
return "fixture response"
def dispatch_unchecked(call: ToolCall, tools: dict):
# The mock records a string; it makes no network request.
return tools[call.name](**call.arguments)
mock_fetch = MockFetch()
call = ToolCall("fetch_url", {"url": "https://docs.example.invalid/"})
assert dispatch_unchecked(call, {"fetch_url": mock_fetch}) == "fixture response"
assert mock_fetch.calls == ["https://docs.example.invalid/"]
# SAFE DEMO: the agent receives only a read-only PR capability
from dataclasses import dataclass
@dataclass(frozen=True)
class ReviewContext:
repository: str
pull_request: int
@dataclass
class ToolCall:
name: str
arguments: dict[str, object]
READ_ONLY_TOOLS = {"read_pr_diff"}
def dispatch_review_call(call: ToolCall, context: ReviewContext, policy, tools):
if call.name not in READ_ONLY_TOOLS:
raise PermissionError("Tool is not available to this review")
expected = {"repository": context.repository,
"pull_request": context.pull_request}
if call.arguments != expected:
raise ValueError("Arguments must match the authorized review")
if not policy.can_read_pr(context.repository, context.pull_request):
raise PermissionError("Review principal cannot read this pull request")
# The injected read_pr_diff function is trusted app code, not model output.
return tools["read_pr_diff"](**expected)
엔지니어링 보안 강화 체크리스트
- 이 시나리오를 확인된 PR 봇 침해 사례처럼 설명하지 마세요. 조건부 설계 위험을 모델링합니다.
- diff만 검토하면 되는 봇에 임의 URL 가져오기 도구를 연결하지 마세요.
- 외부 링크 미리보기가 필요하면 목적지 allowlist, 연결 시 DNS/IP 검증, 리디렉션 재검증, 엄격한 시간·크기 제한, 암묵적 자격 증명 제거를 갖춘 전용 egress 서비스를 사용하세요.
- 네트워크 계층에서 runner의 사설망 및 클라우드 메타데이터 접근을 차단하세요. 필요하지 않으면 메타데이터를 비활성화하고, 필요하면 IMDSv2를 요구하며 컨테이너 hop limit을 검증하세요.
- 봇에 저장소 범위의 읽기 전용 ID를 부여하세요. 신뢰할 수 없는 PR 작업에 비밀을 노출하지 말고 권한이 큰 워크플로와 외부 코드 실행을 분리하세요.
- GitHub Actions에서는 신뢰할 수 없는 코드에 최소 권한 `pull_request`를 우선하세요. `pull_request_target`은 권한이 큰 이벤트로 취급하고 fork 코드를 그 비밀 또는 쓰기 토큰으로 실행하지 마세요.
- 거부된 도구 호출과 목적지 정책 결과를 기록하되 토큰, 전체 비공개 diff, 민감한 응답 본문은 저장하지 마세요.