Prevent xss in preview aand load resources from known URL (#18978)
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
This commit is contained in:
co-authored by
Hariom Balhara
parent
1232eefece
commit
288248d94f
+1
-1
@@ -65,7 +65,7 @@
|
||||
"tdd": "vitest watch",
|
||||
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
||||
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
||||
"e2e:embed": "QUICK=true yarn playwright test --project=@calcom/embed-core",
|
||||
"e2e:embed": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/embed-core",
|
||||
"e2e:embed-react": "QUICK=true yarn playwright test --project=@calcom/embed-react",
|
||||
"test-e2e": "yarn db-seed && yarn e2e",
|
||||
"test-e2e:app-store": "yarn db-seed && yarn e2e:app-store",
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe("Preview", () => {
|
||||
test("Preview - embed-core should load from embedLibUrl", async ({ page }) => {
|
||||
// Intentionally pass a URL that will not load to be able to easily test that the embed was loaded from there
|
||||
page.goto(
|
||||
"http://localhost:3000/embed/preview.html?embedLibUrl=http://wronglocalhost:3000/embed/embed.js&bookerUrl=http://localhost:3000&calLink=pro/30min"
|
||||
"http://localhost:3000/embed/preview.html?embedLibUrl=http://localhost:3000/embed/embed-not-found.js&bookerUrl=http://localhost:3000&calLink=pro/30min"
|
||||
);
|
||||
|
||||
const failedRequestUrl = await new Promise<string>((resolve) =>
|
||||
@@ -40,6 +40,6 @@ test.describe("Preview", () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(failedRequestUrl).toBe("http://wronglocalhost:3000/embed/embed.js");
|
||||
expect(failedRequestUrl).toBe("http://localhost:3000/embed/embed-not-found.js");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,72 @@
|
||||
// We can't import @calcom/lib/constants here yet as this file is compiled using Vite
|
||||
const WEBAPP_URL = process.env.EMBED_PUBLIC_WEBAPP_URL ?? "";
|
||||
const EMBED_LIB_URL = process.env.EMBED_PUBLIC_EMBED_LIB_URL ?? WEBAPP_URL;
|
||||
const IS_E2E = process.env.NEXT_PUBLIC_IS_E2E === "1";
|
||||
if (!WEBAPP_URL) {
|
||||
throw new Error("WEBAPP_URL is not set");
|
||||
}
|
||||
|
||||
// Because it is only used in Embed Snippet Generator preview that is accessible through dashboard only which has URL WEBAPP_URL, we are good with this strict restriction
|
||||
if (!IS_E2E && (window.self === window.top || !document.referrer.startsWith(WEBAPP_URL))) {
|
||||
throw new Error(`This page can only be accessed within an iframe from ${WEBAPP_URL}`);
|
||||
}
|
||||
|
||||
// It is a copy of isSafeUrlToLoadResourceFrom in packages/lib/getSafeRedirectUrl. Keep it in sync
|
||||
// We can't import that here has it is loaded in a separate Vanilla JS page
|
||||
function isSafeUrlToLoadResourceFrom(urlString: string) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow localhost for development
|
||||
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const webappUrl = new URL(WEBAPP_URL);
|
||||
const embedLibUrl = new URL(EMBED_LIB_URL);
|
||||
|
||||
const urlTldPlus1 = getTldPlus1(url.hostname);
|
||||
const webappTldPlus1 = getTldPlus1(webappUrl.hostname);
|
||||
const embedLibTldPlus1 = getTldPlus1(embedLibUrl.hostname);
|
||||
|
||||
// URLs must share the same TLD+1
|
||||
return [webappTldPlus1, embedLibTldPlus1].includes(urlTldPlus1);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTldPlus1(hostname: string) {
|
||||
// Note: It doesn't support multipart tlds like .co.uk and thus makes only one part tld's safe like .com(and thus cal.com)
|
||||
// If we want to use it elsewhere as well(apart from embed/preview.ts) we must consider Public Suffix List
|
||||
return hostname.split(".").slice(-2).join(".");
|
||||
}
|
||||
}
|
||||
|
||||
const searchParams = new URL(document.URL).searchParams;
|
||||
const embedType = searchParams.get("embedType");
|
||||
const calLink = searchParams.get("calLink");
|
||||
const bookerUrl = searchParams.get("bookerUrl");
|
||||
const embedLibUrl = searchParams.get("embedLibUrl");
|
||||
|
||||
if (!bookerUrl || !embedLibUrl) {
|
||||
throw new Error('Can\'t Preview: Missing "bookerUrl" or "embedLibUrl" query parameter');
|
||||
}
|
||||
|
||||
if (!isSafeUrlToLoadResourceFrom(embedLibUrl)) {
|
||||
throw new Error('Invalid "embedLibUrl".');
|
||||
}
|
||||
|
||||
if (!isSafeUrlToLoadResourceFrom(bookerUrl)) {
|
||||
throw new Error('Invalid "bookerUrl".');
|
||||
}
|
||||
|
||||
if (!calLink) {
|
||||
throw new Error('Missing "calLink" query parameter');
|
||||
}
|
||||
|
||||
// TODO: Reuse the embed code snippet from the embed-snippet package - Not able to use it because of circular dependency
|
||||
// Install Cal Embed Code Snippet
|
||||
(function (C, A, L) {
|
||||
@@ -56,9 +117,6 @@ previewWindow.Cal("init", {
|
||||
origin: bookerUrl,
|
||||
});
|
||||
|
||||
if (!calLink) {
|
||||
throw new Error('Missing "calLink" query parameter');
|
||||
}
|
||||
if (embedType === "inline") {
|
||||
previewWindow.Cal("inline", {
|
||||
elementOrSelector: "#my-embed",
|
||||
|
||||
@@ -16,6 +16,8 @@ module.exports = defineConfig((configEnv) => {
|
||||
EMBED_PUBLIC_EMBED_VERSION: embedCoreEnvVars.EMBED_PUBLIC_EMBED_VERSION,
|
||||
EMBED_PUBLIC_VERCEL_URL: embedCoreEnvVars.EMBED_PUBLIC_VERCEL_URL,
|
||||
EMBED_PUBLIC_WEBAPP_URL: embedCoreEnvVars.EMBED_PUBLIC_WEBAPP_URL,
|
||||
EMBED_PUBLIC_EMBED_LIB_URL: embedCoreEnvVars.EMBED_PUBLIC_EMBED_LIB_URL,
|
||||
NEXT_PUBLIC_IS_E2E: embedCoreEnvVars.NEXT_PUBLIC_IS_E2E,
|
||||
}),
|
||||
...(process.argv.includes("--https") ? [basicSsl()] : []),
|
||||
],
|
||||
|
||||
@@ -34,6 +34,8 @@ export const embedCoreEnvVars = {
|
||||
EMBED_PUBLIC_EMBED_FINGER_PRINT: process.env.EMBED_PUBLIC_EMBED_FINGER_PRINT ?? "",
|
||||
EMBED_PUBLIC_EMBED_VERSION: process.env.EMBED_PUBLIC_EMBED_VERSION ?? "",
|
||||
EMBED_PUBLIC_VERCEL_URL: process.env.EMBED_PUBLIC_VERCEL_URL ?? "http://localhost:3000",
|
||||
EMBED_PUBLIC_EMBED_LIB_URL: process.env.EMBED_PUBLIC_EMBED_LIB_URL ?? "",
|
||||
NEXT_PUBLIC_IS_E2E: process.env.NEXT_PUBLIC_IS_E2E ?? "",
|
||||
EMBED_PUBLIC_WEBAPP_URL:
|
||||
process.env.EMBED_PUBLIC_WEBAPP_URL ??
|
||||
(process.env.EMBED_PUBLIC_VERCEL_URL
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import { isSafeUrlToLoadResourceFrom } from "./getSafeRedirectUrl";
|
||||
|
||||
vi.mock("./constants", () => ({
|
||||
WEBAPP_URL: "https://app.cal.com",
|
||||
EMBED_LIB_URL: "https://embed.com",
|
||||
}));
|
||||
|
||||
describe("isSafeUrlToLoadResourceFrom", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("should return true for URLs with same TLD+1 as WEBAPP_URL", () => {
|
||||
expect(isSafeUrlToLoadResourceFrom("https://acme.cal.com/path")).toBe(true);
|
||||
expect(isSafeUrlToLoadResourceFrom("http://dunder.cal.com?query=1")).toBe(true);
|
||||
expect(isSafeUrlToLoadResourceFrom("https://cal.com/path")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for URLs with same TLD+1 as EMBED_LIB_URL", () => {
|
||||
expect(isSafeUrlToLoadResourceFrom("https://test.embed.com/path")).toBe(true);
|
||||
expect(isSafeUrlToLoadResourceFrom("http://embed.com/script.js")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for localhost URLs", () => {
|
||||
expect(isSafeUrlToLoadResourceFrom("http://localhost:3000")).toBe(true);
|
||||
expect(isSafeUrlToLoadResourceFrom("http://127.0.0.1:3000")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for different TLD+1", () => {
|
||||
expect(isSafeUrlToLoadResourceFrom("https://malicious1.com")).toBe(false);
|
||||
expect(isSafeUrlToLoadResourceFrom("https://malicious2.com")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for non-http/https protocols", () => {
|
||||
expect(isSafeUrlToLoadResourceFrom("ftp://example.com")).toBe(false);
|
||||
expect(isSafeUrlToLoadResourceFrom("javascript:alert(1)")).toBe(false);
|
||||
expect(isSafeUrlToLoadResourceFrom("data:text/html,<script>alert(1)</script>")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for invalid URLs", () => {
|
||||
expect(isSafeUrlToLoadResourceFrom("not-a-url")).toBe(false);
|
||||
expect(isSafeUrlToLoadResourceFrom("http://")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL, EMBED_LIB_URL } from "@calcom/lib/constants";
|
||||
|
||||
// It ensures that redirection URL safe where it is accepted through a query params or other means where user can change it.
|
||||
export const getSafeRedirectUrl = (url = "") => {
|
||||
@@ -20,3 +20,36 @@ export const getSafeRedirectUrl = (url = "") => {
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
// There is a copy of this fn at packages/embed/embed-core/src/preview.ts as that can't import this function. Keep it in sync
|
||||
export function isSafeUrlToLoadResourceFrom(urlString: string) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow localhost for development
|
||||
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const webappUrl = new URL(WEBAPP_URL);
|
||||
const embedLibUrl = new URL(EMBED_LIB_URL);
|
||||
|
||||
const urlTldPlus1 = getTldPlus1(url.hostname);
|
||||
const webappTldPlus1 = getTldPlus1(webappUrl.hostname);
|
||||
const embedLibTldPlus1 = getTldPlus1(embedLibUrl.hostname);
|
||||
|
||||
// URLs must share the same TLD+1 so that org domains are also allowed.
|
||||
return [webappTldPlus1, embedLibTldPlus1].includes(urlTldPlus1);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTldPlus1(hostname: string) {
|
||||
// Note: It doesn't support multipart tlds like .co.uk and thus makes only one part tld's safe like .com(and thus cal.com)
|
||||
// If we want to use it elsewhere as well(apart from embed/preview.ts) we must consider Public Suffix List
|
||||
return hostname.split(".").slice(-2).join(".");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,6 +438,7 @@
|
||||
"EMBED_PUBLIC_EMBED_VERSION",
|
||||
"EMBED_PUBLIC_WEBAPP_URL",
|
||||
"EMBED_PUBLIC_VERCEL_URL",
|
||||
"EMBED_PUBLIC_EMBED_LIB_URL",
|
||||
"NEXT_PUBLIC_ENABLE_PROFILE_SWITCHER",
|
||||
"NEXT_PUBLIC_HEAD_SCRIPTS",
|
||||
"NEXT_PUBLIC_BODY_SCRIPTS",
|
||||
|
||||
Reference in New Issue
Block a user