Skip to content

Commit 3025cf5

Browse files
committed
Validate workflow URLs better
1 parent 60e9e81 commit 3025cf5

File tree

2 files changed

+49
-12
lines changed

2 files changed

+49
-12
lines changed

packages/sdk/src/server/__tests__/server.test.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,13 @@ describe("BackendClient", () => {
507507
});
508508

509509
describe("invokeWorkflow", () => {
510+
beforeEach(() => {
511+
client = new BackendClient({
512+
...clientParams,
513+
workflowDomain: "example.com",
514+
});
515+
});
516+
510517
it("should invoke a workflow with provided URL and body, with no auth type", async () => {
511518
fetchMock.mockResponseOnce(
512519
JSON.stringify({
@@ -675,15 +682,10 @@ describe("BackendClient", () => {
675682
let client: BackendClient;
676683

677684
beforeEach(() => {
678-
client = new BackendClient(
679-
{
680-
credentials: {
681-
clientId: "test-client-id",
682-
clientSecret: "test-client-secret",
683-
},
684-
projectId,
685-
},
686-
);
685+
client = new BackendClient({
686+
...clientParams,
687+
workflowDomain: "example.com",
688+
});
687689
});
688690

689691
it("should include externalUserId and environment headers", async () => {
@@ -729,6 +731,22 @@ describe("BackendClient", () => {
729731
});
730732

731733
describe("BackendClient - buildWorkflowUrl", () => {
734+
describe("Validations", () => {
735+
it("should throw an error when the input is blank", () => {
736+
expect(() => client["buildWorkflowUrl"](" ")).toThrow("URL or endpoint ID is required");
737+
});
738+
739+
it("should throw an error when the URL doesn't match the workflow domain", () => {
740+
const url = "https://example.com";
741+
expect(() => client["buildWorkflowUrl"](url)).toThrow("Invalid workflow domain");
742+
});
743+
744+
it("should throw an error when the endpoint ID doesn't match the expected format", () => {
745+
const input = "foo123";
746+
expect(() => client["buildWorkflowUrl"](input)).toThrow("Invalid endpoint ID format");
747+
});
748+
});
749+
732750
describe("Default domain (m.pipedream.net)", () => {
733751
it("should return full URL if input is a full URL with protocol", () => {
734752
const input = "https://en123.m.pipedream.net";

packages/sdk/src/server/index.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -693,23 +693,42 @@ export class BackendClient {
693693
* ```
694694
*/
695695
private buildWorkflowUrl(input: string): string {
696+
if (!input?.trim()) {
697+
throw new Error("URL or endpoint ID is required");
698+
}
699+
700+
input = input.trim().toLowerCase();
696701
let url: string;
697702

698703
const isUrl = input.includes(".") || input.startsWith("http");
699704

700705
if (isUrl) {
701-
// Try to parse the input as a URL
706+
// Try to parse the input as a URL
707+
let parsedUrl: URL;
702708
try {
703709
const urlString = input.startsWith("http")
704710
? input
705711
: `https://${input}`;
706-
const parsedUrl = new URL(urlString);
707-
url = parsedUrl.href;
712+
parsedUrl = new URL(urlString);
708713
} catch (error) {
709714
throw new Error(`The provided URL is malformed: "${input}". Please provide a valid URL.`);
710715
}
716+
717+
// Validate the hostname to prevent potential DNS rebinding attacks
718+
if (!parsedUrl.hostname.endsWith(this.workflowDomain)) {
719+
throw new Error(`Invalid workflow domain. URL must end with ${this.workflowDomain}`);
720+
}
721+
722+
url = parsedUrl.href;
711723
} else {
712724
// If the input is an ID, construct the full URL using the base domain
725+
if (!/^e(n|o)[a-z0-9-]+$/i.test(input)) {
726+
throw new Error(`
727+
Invalid endpoint ID format.
728+
Must contain only letters, numbers, and hyphens, and start with either "en" or "eo".
729+
`);
730+
}
731+
713732
url = `https://${input}.${this.workflowDomain}`;
714733
}
715734

0 commit comments

Comments
 (0)