Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -663,11 +663,14 @@ def build_url_with_params(
def is_service_registered(url: str, registered_services: set[str]) -> bool:
"""Check if a URL is registered for x402 requests.

Matches by origin (protocol + hostname + port) or prefix.
Matches by origin (protocol + hostname + port) or by same-origin path prefix.

Does **not** use raw ``url.startswith(registered)`` — that allows hostname-suffix
and other prefix bypasses (e.g. ``https://good.com.evil.com`` vs ``https://good.com``).

Args:
url: The URL to check
registered_services: Set of registered service URLs
registered_services: Set of registered service URLs or origins

Returns:
True if the service is registered, False otherwise
Expand All @@ -678,12 +681,34 @@ def is_service_registered(url: str, registered_services: set[str]) -> bool:

try:
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
return False
origin = f"{parsed.scheme}://{parsed.netloc}"

for registered in registered_services:
# Check if origin matches or URL starts with registered prefix
if origin == registered or url.startswith(registered):
# Bare origin registration: "https://api.example.com"
if origin == registered:
return True

reg = urlparse(registered)
if not reg.scheme or not reg.netloc:
continue

reg_origin = f"{reg.scheme}://{reg.netloc}"
if origin != reg_origin:
continue

# Origin-only URL form ("https://api.example.com/") → any path on that origin
reg_path = reg.path or "/"
if reg_path == "/" and not reg.query and not reg.fragment:
return True

# Same-origin path match or segment-boundary prefix
req_path = parsed.path or "/"
reg_prefix = reg_path if reg_path.endswith("/") else f"{reg_path}/"
if req_path == reg_path or req_path.startswith(reg_prefix):
return True

return False
except Exception:
return False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { isServiceRegistered } from "./utils";

describe("isServiceRegistered", () => {
const registered = new Set(["https://api.example.com", "https://pay.example.com/v1"]);

it("allows exact origin registration for any path on that origin", () => {
expect(isServiceRegistered("https://api.example.com/foo", registered)).toBe(true);
});

it("allows same-origin path prefix registrations", () => {
expect(isServiceRegistered("https://pay.example.com/v1/charge", registered)).toBe(true);
expect(isServiceRegistered("https://pay.example.com/v1", registered)).toBe(true);
});

it("rejects hostname-suffix bypass of prefix matching", () => {
expect(isServiceRegistered("https://api.example.com.evil.com/x", registered)).toBe(false);
});

it("rejects different hosts even when the string shares a prefix", () => {
expect(isServiceRegistered("https://api.example.com.attacker/x", registered)).toBe(false);
expect(isServiceRegistered("https://evil.com/https://api.example.com", registered)).toBe(
false,
);
});

it("rejects sibling paths outside the registered prefix", () => {
expect(isServiceRegistered("https://pay.example.com/v2/charge", registered)).toBe(false);
expect(isServiceRegistered("https://pay.example.com/v10", registered)).toBe(false);
});
});
34 changes: 29 additions & 5 deletions typescript/agentkit/src/action-providers/x402/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,10 +615,13 @@ export function buildUrlWithParams(

/**
* Checks if a URL is registered for x402 requests.
* Matches by origin (protocol + hostname + port) or prefix.
* Matches by origin (protocol + hostname + port) or by same-origin path prefix.
*
* Does **not** use raw `url.startsWith(registered)` — that allows hostname-suffix
* and other prefix bypasses (e.g. `https://good.com.evil.com` vs `https://good.com`).
*
* @param url - The URL to check
* @param registeredServices - Set of registered service URLs
* @param registeredServices - Set of registered service URLs or origins
* @returns True if the service is registered, false otherwise
*/
export function isServiceRegistered(url: string, registeredServices: Set<string>): boolean {
Expand All @@ -628,11 +631,32 @@ export function isServiceRegistered(url: string, registeredServices: Set<string>

try {
const parsed = new URL(url);
const origin = parsed.origin;

for (const registered of registeredServices) {
// Check if origin matches or URL starts with registered prefix
if (origin === registered || url.startsWith(registered)) {
// Bare origin registration: "https://api.example.com"
if (parsed.origin === registered) {
return true;
}

let reg: URL;
try {
reg = new URL(registered);
} catch {
continue;
}

if (parsed.origin !== reg.origin) {
continue;
}

// Origin-only URL form ("https://api.example.com/") → any path on that origin
if (reg.pathname === "/" && reg.search === "" && reg.hash === "") {
return true;
}

// Same-origin path match or segment-boundary prefix
const regPrefix = reg.pathname.endsWith("/") ? reg.pathname : `${reg.pathname}/`;
if (parsed.pathname === reg.pathname || parsed.pathname.startsWith(regPrefix)) {
return true;
}
}
Expand Down
Loading