🐛 Fix upload proxy public URL (#2508)

- Prevent signed upload proxy URLs from using internal request origins
in self-hosted reverse-proxy setups.
- Resolve runtime upload proxy URLs from `NEXT_PUBLIC_VIEWER_URL` and
builder upload proxy URLs from `NEXTAUTH_URL`.
- Add regression coverage for internal container origins like
`https://2e862faf612f:3000`.
This commit is contained in:
Baptiste Arnaud 2026-05-22 15:56:26 +02:00 committed by GitHub
parent 5f01ecff64
commit 9d6708bbee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 74 additions and 2 deletions

View File

@ -7,6 +7,7 @@ import {
parseUploadPathSegment,
resolveUploadFileType,
} from "@typebot.io/lib/s3/createUploadFilePath";
import { resolveUploadProxyBaseUrl } from "@typebot.io/lib/s3/resolveUploadProxyBaseUrl";
import { generateSignedUploadProxyUrl } from "@typebot.io/lib/s3/signedUploadProxy";
import prisma from "@typebot.io/prisma";
import { z } from "zod";
@ -79,7 +80,11 @@ export const generateUploadUrl = authenticatedProcedure
});
return generateSignedUploadProxyUrl({
baseUrl: apiOrigin ?? env.NEXTAUTH_URL,
baseUrl: resolveUploadProxyBaseUrl({
publicBaseUrls: [env.NEXTAUTH_URL],
fallbackBaseUrl: env.NEXTAUTH_URL,
requestOrigin: apiOrigin,
}),
fileType: resolvedFileType,
filePath,
});

View File

@ -1,4 +1,9 @@
import { env } from "@typebot.io/env";
import { resolveUploadProxyBaseUrl } from "@typebot.io/lib/s3/resolveUploadProxyBaseUrl";
export const getUploadProxyBaseUrl = (apiOrigin: string | undefined) =>
apiOrigin ?? env.NEXT_PUBLIC_VIEWER_URL[0] ?? env.NEXTAUTH_URL;
resolveUploadProxyBaseUrl({
publicBaseUrls: env.NEXT_PUBLIC_VIEWER_URL,
fallbackBaseUrl: env.NEXTAUTH_URL,
requestOrigin: apiOrigin,
});

View File

@ -0,0 +1,36 @@
import { describe, expect, it } from "bun:test";
import { resolveUploadProxyBaseUrl } from "./resolveUploadProxyBaseUrl";
describe("resolveUploadProxyBaseUrl", () => {
it("uses the matching configured public URL when the request origin is public", () => {
expect(
resolveUploadProxyBaseUrl({
publicBaseUrls: [
"https://viewer.example.com",
"https://bot.example.com",
],
fallbackBaseUrl: "https://builder.example.com",
requestOrigin: "https://bot.example.com",
}),
).toBe("https://bot.example.com");
});
it("ignores an internal request origin and uses the first configured public URL", () => {
expect(
resolveUploadProxyBaseUrl({
publicBaseUrls: ["https://bot.example.com"],
fallbackBaseUrl: "https://builder.example.com",
requestOrigin: "https://2e862faf612f:3000",
}),
).toBe("https://bot.example.com");
});
it("falls back when no configured public URL is available", () => {
expect(
resolveUploadProxyBaseUrl({
publicBaseUrls: [],
fallbackBaseUrl: "https://builder.example.com",
}),
).toBe("https://builder.example.com");
});
});

View File

@ -0,0 +1,26 @@
type ResolveUploadProxyBaseUrlProps = {
publicBaseUrls: string[];
fallbackBaseUrl: string;
requestOrigin?: string;
};
export const resolveUploadProxyBaseUrl = ({
publicBaseUrls,
fallbackBaseUrl,
requestOrigin,
}: ResolveUploadProxyBaseUrlProps) =>
(requestOrigin
? publicBaseUrls.find(
(publicBaseUrl) => parseOrigin(publicBaseUrl) === requestOrigin,
)
: undefined) ??
publicBaseUrls[0] ??
fallbackBaseUrl;
const parseOrigin = (url: string) => {
try {
return new URL(url).origin;
} catch {
return;
}
};