fix(security): validate bedrock endpoint URLs

This commit is contained in:
ZacharyZhang-NY committed 2026-05-15 00:52:18 -04:00
1 parent 84b92c859e
commit 8b0b06f765
7 files changed
+141 -8

No files matched your search

+11 -2
View File
@@ -1,6 +1,6 @@
import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
import { getEnvApiKey } from "@mariozechner/pi-ai";
import { isLocalConnectorBaseUrl } from "@shared/contracts/schemas-credentials";
import { isAwsBedrockEndpointUrl, isLocalConnectorBaseUrl } from "@shared/contracts/schemas-credentials";
import { getConnectorDefinition, isPiKnownProvider } from "@main/connectors";
import { UpstreamError } from "../errors/index.js";
import { CredentialStore } from "./credential-store";
@@ -133,7 +133,16 @@ export class CredentialResolver {
env.AWS_BEARER_TOKEN_BEDROCK = cfg.bearerToken;
}
if (cfg.forceCache) env.AWS_BEDROCK_FORCE_CACHE = "1";
if (cfg.endpointUrl) env.AWS_ENDPOINT_URL_BEDROCK_RUNTIME = cfg.endpointUrl;
if (cfg.endpointUrl) {
if (!isAwsBedrockEndpointUrl(cfg.endpointUrl)) {
throw new CredentialError(
"CREDENTIAL_INVALID",
connectorId,
"AWS Bedrock endpointUrl must be an HTTPS AWS Bedrock runtime endpoint or a private endpoint.",
);
}
env.AWS_ENDPOINT_URL_BEDROCK_RUNTIME = cfg.endpointUrl;
}
if (cfg.skipAuth) env.AWS_BEDROCK_SKIP_AUTH = "1";
if (cfg.forceHttp1) env.AWS_BEDROCK_FORCE_HTTP1 = "1";
return { apiKey: "", extraEnv: env };
@@ -1,4 +1,5 @@
import { useState } from "react";
import { isAwsBedrockEndpointUrl } from "@shared/contracts/schemas-credentials";
import { ActionButton, Input, Select } from "../../ui";
import { useT } from "../../../i18n";
@@ -43,6 +44,11 @@ export function AwsForm({ connectorId, onSaved }: Props) {
setError(t("form.aws.errBearerRequired"));
return;
}
const trimmedEndpointUrl = endpointUrl.trim();
if (trimmedEndpointUrl && !isAwsBedrockEndpointUrl(trimmedEndpointUrl)) {
setError(t("form.aws.errEndpointUrlInvalid"));
return;
}
setBusy(true);
setError(null);
const result = await desktop.saveCredential({
@@ -59,7 +65,7 @@ export function AwsForm({ connectorId, onSaved }: Props) {
authMode === "iam" && sessionToken.trim() ? sessionToken.trim() : undefined,
bearerToken: authMode === "bearer" ? bearerToken.trim() : undefined,
forceCache,
endpointUrl: endpointUrl.trim() || undefined,
endpointUrl: trimmedEndpointUrl || undefined,
skipAuth,
forceHttp1,
},
+1
View File
@@ -1181,6 +1181,7 @@ export const en: Record<string, string> = {
"form.aws.errProfileRequired": "AWS profile name required.",
"form.aws.errIamRequired": "Access key id and secret access key required for IAM auth.",
"form.aws.errBearerRequired": "Bearer token required.",
"form.aws.errEndpointUrlInvalid": "Endpoint URL must be an HTTPS AWS Bedrock runtime endpoint or a private endpoint.",
"form.azure.apiKey": "API key",
"form.azure.baseUrl": "Base URL (optional)",
"form.azure.resourceName": "Resource name (optional)",
+1
View File
@@ -1181,6 +1181,7 @@ export const zh: Record<string, string> = {
"form.aws.errProfileRequired": "请填写 AWS Profile 名称。",
"form.aws.errIamRequired": "IAM 认证需要 Access Key ID 和 Secret Access Key。",
"form.aws.errBearerRequired": "请填写 Bearer Token。",
"form.aws.errEndpointUrlInvalid": "Endpoint URL 必须是 HTTPS AWS Bedrock Runtime endpoint 或私有 endpoint。",
"form.azure.apiKey": "API Key",
"form.azure.baseUrl": "Base URL(可选)",
"form.azure.resourceName": "资源名称(可选)",
+51 -3
View File
@@ -22,6 +22,14 @@ export const apiKeyValueSchema = z.object({
value: z.string().min(1),
});
const AWS_REGION_SEGMENT = "[a-z]{2}(?:-gov)?-[a-z]+-\\d";
const AWS_BEDROCK_RUNTIME_HOSTNAME = new RegExp(
`^bedrock-runtime(?:-fips)?\\.${AWS_REGION_SEGMENT}\\.amazonaws\\.com(?:\\.cn)?$`,
);
const AWS_BEDROCK_VPCE_HOSTNAME = new RegExp(
`^vpce-[a-z0-9-]+\\.bedrock-runtime(?:-fips)?\\.${AWS_REGION_SEGMENT}\\.vpce\\.amazonaws\\.com(?:\\.cn)?$`,
);
export const azureCredentialSchema = z.object({
apiKey: z.string().min(1),
baseUrl: z.string().url().optional().or(z.literal("")),
@@ -30,6 +38,14 @@ export const azureCredentialSchema = z.object({
deploymentMap: z.record(z.string(), z.string()).optional(),
});
export const awsEndpointUrlSchema = z
.string()
.url()
.refine(
isAwsBedrockEndpointUrl,
"AWS Bedrock endpointUrl must be an HTTPS AWS Bedrock runtime endpoint or a private endpoint.",
);
export const awsCredentialSchema = z.object({
authMode: z.enum(["profile", "iam", "bearer"]),
region: z.string().min(1).default("us-east-1"),
@@ -39,7 +55,7 @@ export const awsCredentialSchema = z.object({
sessionToken: z.string().optional(),
bearerToken: z.string().optional(),
forceCache: z.boolean().optional(),
endpointUrl: z.string().optional(),
endpointUrl: awsEndpointUrlSchema.optional(),
skipAuth: z.boolean().optional(),
forceHttp1: z.boolean().optional(),
});
@@ -85,6 +101,21 @@ function isPrivateIpv6(hostname: string): boolean {
return /^f[cd][0-9a-f]{2}:/i.test(hostname) || /^fe80:/i.test(hostname);
}
function isPrivateNetworkHostname(hostname: string): boolean {
return hostname === "localhost" || isPrivateIpv4(hostname) || isPrivateIpv6(hostname);
}
function hasOnlyRootUrlPath(value: string): boolean {
const trimmed = value.trim();
if (trimmed.includes("?") || trimmed.includes("#")) return false;
const schemeSeparator = trimmed.indexOf("://");
if (schemeSeparator < 0) return false;
const authorityAndPath = trimmed.slice(schemeSeparator + 3);
const pathStart = authorityAndPath.indexOf("/");
if (pathStart < 0) return true;
return authorityAndPath.slice(pathStart) === "/";
}
export function isLocalConnectorBaseUrl(value: string): boolean {
let url: URL;
try {
@@ -95,8 +126,7 @@ export function isLocalConnectorBaseUrl(value: string): boolean {
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
if (url.username || url.password) return false;
const hostname = normalizeUrlHostname(url.hostname);
if (hostname === "localhost") return true;
return isPrivateIpv4(hostname) || isPrivateIpv6(hostname);
return isPrivateNetworkHostname(hostname);
}
export const localBaseUrlSchema = z
@@ -111,6 +141,24 @@ export const localCredentialSchema = z.object({
baseUrl: localBaseUrlSchema,
});
function isAwsBedrockHostname(hostname: string): boolean {
return AWS_BEDROCK_RUNTIME_HOSTNAME.test(hostname) || AWS_BEDROCK_VPCE_HOSTNAME.test(hostname);
}
export function isAwsBedrockEndpointUrl(value: string): boolean {
let url: URL;
try {
url = new URL(value);
} catch {
return false;
}
if (url.protocol !== "https:") return false;
if (url.username || url.password) return false;
if (!hasOnlyRootUrlPath(value)) return false;
const hostname = normalizeUrlHostname(url.hostname);
return isAwsBedrockHostname(hostname) || isPrivateNetworkHostname(hostname);
}
export const credentialPayloadSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("api_key"), value: apiKeyValueSchema }),
z.object({ kind: z.literal("azure"), value: azureCredentialSchema }),