fix: urls.unfurl does not correctly treat as internal on FQD (#13189)

This commit is contained in:
Tom Moor
2026-07-28 21:54:17 -04:00
committed by GitHub
parent 7bc6a3ca79
commit f8ccdf143c
3 changed files with 97 additions and 4 deletions
+19 -1
View File
@@ -28,7 +28,11 @@ import { isEmail } from "validator";
import { TeamPreferenceDefaults } from "@shared/constants";
import type { TeamPreferences } from "@shared/types";
import { TeamPreference, UserRole } from "@shared/types";
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
import {
getBaseDomain,
parseDomain,
RESERVED_SUBDOMAINS,
} from "@shared/utils/domains";
import { attachmentRedirectRegex } from "@shared/utils/ProsemirrorHelper";
import { parseEmail } from "@shared/utils/email";
import { TeamValidation } from "@shared/validations";
@@ -285,6 +289,20 @@ class Team extends ParanoidModel<
return url.href.replace(/\/$/, "");
}
/**
* Returns whether the given url points at this team's installation, taking
* into account custom domains and hosted subdomains.
*
* @param url The url to check.
* @returns True if the url belongs to this team.
*/
public isTeamUrl(url: string): boolean {
if (!url) {
return false;
}
return parseDomain(url).host === parseDomain(this.url).host;
}
/**
* Returns a code that can be used to delete the user's team. The code will
* be rotated when the user signs out.
+70
View File
@@ -1,4 +1,5 @@
import type { Mock } from "vitest";
import { randomString } from "@shared/random";
import { UnfurlResourceType } from "@shared/types";
import env from "@server/env";
import type { User } from "@server/models";
@@ -6,6 +7,7 @@ import {
buildCollection,
buildDocument,
buildShare,
buildTeam,
buildUser,
} from "@server/test/factories";
import { getTestServer } from "@server/test/support";
@@ -207,6 +209,74 @@ Usage instructions here.`,
expect(body.summary).toEqual(
`## Installation
Install instructions here.`
);
});
it("should succeed when document url is on the team custom domain", async () => {
const team = await buildTeam({
domain: `${randomString(10)}.example.com`,
});
const teamUser = await buildUser({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
userId: teamUser.id,
text: `Intro paragraph.
## Installation
Install instructions here.`,
});
const res = await server.post("/api/urls.unfurl", teamUser, {
body: {
url: `${team.url}/${document.url}#h-installation`,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.type).toEqual(UnfurlResourceType.Document);
expect(body.id).toEqual(document.id);
expect(body.summary).toEqual(
`## Installation
Install instructions here.`
);
});
it("should succeed when document url is on the team subdomain", async () => {
vi.spyOn(env, "isCloudHosted", "get").mockReturnValue(true);
const team = await buildTeam({
subdomain: randomString({
length: 10,
charset: "alphabetic",
capitalization: "lowercase",
}),
});
const teamUser = await buildUser({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
userId: teamUser.id,
text: `Intro paragraph.
## Installation
Install instructions here.`,
});
const res = await server.post("/api/urls.unfurl", teamUser, {
body: {
url: `${team.url}/${document.url}#h-installation`,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.type).toEqual(UnfurlResourceType.Document);
expect(body.id).toEqual(document.id);
expect(body.summary).toEqual(
`## Installation
Install instructions here.`
);
});
+8 -3
View File
@@ -3,7 +3,7 @@ import Router from "koa-router";
import { traceFunction } from "@server/logging/tracing";
import isUUID from "validator/lib/isUUID";
import { MentionType, UnfurlResourceType } from "@shared/types";
import { getBaseDomain, parseDomain } from "@shared/utils/domains";
import { getBaseDomain } from "@shared/utils/domains";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import parseMentionUrl from "@shared/utils/parseMentionUrl";
import { isInternalUrl, parseShareIdFromUrl } from "@shared/utils/urls";
@@ -45,8 +45,13 @@ router.post(
const { url, documentId } = ctx.input.body;
const urlObj = new URL(url);
// A url may point at this installation without matching the default env.URL
// when the team is on a custom domain or hosted subdomain.
const isTeamUrl =
isInternalUrl(url) || !!ctx.state.auth.user?.team.isTeamUrl(url);
// Public share URLs does not require authentication
if (isInternalUrl(url)) {
if (isTeamUrl) {
const shareId = parseShareIdFromUrl(url);
if (shareId) {
@@ -159,7 +164,7 @@ router.post(
}
// Internal resources
if (isInternalUrl(url) || parseDomain(url).host === actor.team.domain) {
if (isTeamUrl) {
const previewDocumentId = parseDocumentSlug(url);
if (previewDocumentId) {
const document = await Document.findByPk(previewDocumentId, {