flawopen.com/Reference/PR bot security
AI PR Review Bots: Prevent Prompt-Injection SSRF
How untrusted pull-request text can influence AI tools, where SSRF risk begins, and how to constrain bot permissions and network access.
ELI5 — A review assistant with a company network pass
A pull request is like a package delivered to a review desk: its contents may be useful, but they do not become company policy. If an AI reviewer can also fetch arbitrary URLs from inside a private network, a misleading instruction may influence where its tool looks. The risk depends on the tool and network permissions; reading hostile text alone does not grant network access.
Core concepts and key terms
- Untrusted input:
- Pull-request titles, descriptions, comments, diffs, and linked content from contributors.
- Indirect prompt injection:
- Untrusted text influences an agent because it is included in the model context.
- SSRF boundary:
- A server-side fetch tool can reach destinations the contributor cannot reach directly.
- Impact conditions:
- A reachable internal service, available credentials, and inadequate authorization or egress controls must also be present.
Mechanism and root cause
A review bot may combine contributor-controlled text with trusted review instructions. The model can propose a tool call based on either. Prompt wording and delimiters do not authorize network access. SSRF becomes possible only when application code accepts a model-selected destination and a network-capable tool can reach it. Access to cloud metadata or credentials is a further conditional impact, not an automatic result of prompt injection.
Conditional attack path
Contributor controls PR content
An external contributor can influence titles, descriptions, comments, changed text, and linked material.
Agent reads that content
The service places some untrusted fields into model context; this can influence the model’s response or tool proposal.
Tool capability determines reach
If the agent can fetch arbitrary URLs, the runner may contact destinations allowed by its network path. Without that tool, this SSRF path is absent.
Impact depends on deployment
Internal data or credentials are exposed only if reachable services, metadata settings, identity permissions, and output controls permit it.
Source code: unchecked dispatch vs. authorized read capability
# 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)
Engineering hardening checklist
- Do not claim a confirmed PR-bot incident from this scenario. The page models a conditional design risk.
- Do not attach an arbitrary URL-fetch tool to a reviewer that only needs to inspect a diff.
- If external link previews are required, use a dedicated egress service with destination allowlists, DNS/IP validation at connection time, redirect revalidation, strict time and size limits, and no ambient credentials.
- Block runner access to private networks and cloud metadata at the network layer. Disable instance metadata when the workload does not need it; otherwise require IMDSv2 and validate container hop-limit needs.
- Give the bot a read-only, repository-scoped identity. Keep secrets out of untrusted-PR jobs and separate any privileged workflow from untrusted code execution.
- For GitHub Actions, prefer the least-privileged `pull_request` workflow for untrusted code. Treat `pull_request_target` as privileged and never check out or execute fork code with its secrets or write token.
- Log rejected tool calls and destination policy decisions without recording tokens, full private diffs, or sensitive response bodies.