fix: stronger enforcement of normalizeLink (#6728)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
b7350f9707
commit
b33fa6d6f6
@ -19,6 +19,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
|
@ -291,13 +291,12 @@ import {
|
|||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
import {
|
import {
|
||||||
normalizeLink,
|
|
||||||
showHyperlinkTooltip,
|
showHyperlinkTooltip,
|
||||||
hideHyperlinkToolip,
|
hideHyperlinkToolip,
|
||||||
Hyperlink,
|
Hyperlink,
|
||||||
isPointHittingLinkIcon,
|
isPointHittingLinkIcon,
|
||||||
isLocalLink,
|
|
||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
|
import { isLocalLink, normalizeLink } from "../data/url";
|
||||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||||
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
||||||
import { Fonts } from "../scene/Fonts";
|
import { Fonts } from "../scene/Fonts";
|
||||||
@ -3352,12 +3351,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.device.isMobile,
|
this.device.isMobile,
|
||||||
);
|
);
|
||||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||||
const url = this.hitLinkElement.link;
|
let url = this.hitLinkElement.link;
|
||||||
if (url) {
|
if (url) {
|
||||||
|
url = normalizeLink(url);
|
||||||
let customEvent;
|
let customEvent;
|
||||||
if (this.props.onLinkOpen) {
|
if (this.props.onLinkOpen) {
|
||||||
customEvent = wrapEvent(EVENT.EXCALIDRAW_LINK, event.nativeEvent);
|
customEvent = wrapEvent(EVENT.EXCALIDRAW_LINK, event.nativeEvent);
|
||||||
this.props.onLinkOpen(this.hitLinkElement, customEvent);
|
this.props.onLinkOpen(
|
||||||
|
{
|
||||||
|
...this.hitLinkElement,
|
||||||
|
link: url,
|
||||||
|
},
|
||||||
|
customEvent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!customEvent?.defaultPrevented) {
|
if (!customEvent?.defaultPrevented) {
|
||||||
const target = isLocalLink(url) ? "_self" : "_blank";
|
const target = isLocalLink(url) ? "_self" : "_blank";
|
||||||
@ -3365,7 +3371,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// https://mathiasbynens.github.io/rel-noopener/
|
// https://mathiasbynens.github.io/rel-noopener/
|
||||||
if (newWindow) {
|
if (newWindow) {
|
||||||
newWindow.opener = null;
|
newWindow.opener = null;
|
||||||
newWindow.location = normalizeLink(url);
|
newWindow.location = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import {
|
|||||||
measureBaseline,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { COLOR_PALETTE } from "../colors";
|
import { COLOR_PALETTE } from "../colors";
|
||||||
|
import { normalizeLink } from "./url";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@ -142,7 +143,7 @@ const restoreElementWithProperties = <
|
|||||||
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
|
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
|
||||||
: element.boundElements ?? [],
|
: element.boundElements ?? [],
|
||||||
updated: element.updated ?? getUpdatedTimestamp(),
|
updated: element.updated ?? getUpdatedTimestamp(),
|
||||||
link: element.link ?? null,
|
link: element.link ? normalizeLink(element.link) : null,
|
||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
30
src/data/url.test.tsx
Normal file
30
src/data/url.test.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { normalizeLink } from "./url";
|
||||||
|
|
||||||
|
describe("normalizeLink", () => {
|
||||||
|
// NOTE not an extensive XSS test suite, just to check if we're not
|
||||||
|
// regressing in sanitization
|
||||||
|
it("should sanitize links", () => {
|
||||||
|
expect(
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
normalizeLink(`javascript://%0aalert(document.domain)`).startsWith(
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
`javascript:`,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(normalizeLink("ola")).toBe("ola");
|
||||||
|
expect(normalizeLink(" ola")).toBe("ola");
|
||||||
|
|
||||||
|
expect(normalizeLink("https://www.excalidraw.com")).toBe(
|
||||||
|
"https://www.excalidraw.com",
|
||||||
|
);
|
||||||
|
expect(normalizeLink("www.excalidraw.com")).toBe("www.excalidraw.com");
|
||||||
|
expect(normalizeLink("/ola")).toBe("/ola");
|
||||||
|
expect(normalizeLink("http://test")).toBe("http://test");
|
||||||
|
expect(normalizeLink("ftp://test")).toBe("ftp://test");
|
||||||
|
expect(normalizeLink("file://")).toBe("file://");
|
||||||
|
expect(normalizeLink("file://")).toBe("file://");
|
||||||
|
expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)");
|
||||||
|
expect(normalizeLink("[[test]]")).toBe("[[test]]");
|
||||||
|
expect(normalizeLink("<test>")).toBe("<test>");
|
||||||
|
});
|
||||||
|
});
|
9
src/data/url.ts
Normal file
9
src/data/url.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
|
||||||
|
export const normalizeLink = (link: string) => {
|
||||||
|
return sanitizeUrl(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isLocalLink = (link: string | null) => {
|
||||||
|
return !!(link?.includes(location.origin) || link?.startsWith("/"));
|
||||||
|
};
|
@ -29,6 +29,7 @@ import { getTooltipDiv, updateTooltipPosition } from "../components/Tooltip";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { isPointHittingElementBoundingBox } from "./collision";
|
import { isPointHittingElementBoundingBox } from "./collision";
|
||||||
import { getElementAbsoluteCoords } from "./";
|
import { getElementAbsoluteCoords } from "./";
|
||||||
|
import { isLocalLink, normalizeLink } from "../data/url";
|
||||||
|
|
||||||
import "./Hyperlink.scss";
|
import "./Hyperlink.scss";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
@ -166,7 +167,7 @@ export const Hyperlink = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href={element.link || ""}
|
href={normalizeLink(element.link || "")}
|
||||||
className={clsx("excalidraw-hyperlinkContainer-link", {
|
className={clsx("excalidraw-hyperlinkContainer-link", {
|
||||||
"d-none": isEditing,
|
"d-none": isEditing,
|
||||||
})}
|
})}
|
||||||
@ -177,7 +178,13 @@ export const Hyperlink = ({
|
|||||||
EVENT.EXCALIDRAW_LINK,
|
EVENT.EXCALIDRAW_LINK,
|
||||||
event.nativeEvent,
|
event.nativeEvent,
|
||||||
);
|
);
|
||||||
onLinkOpen(element, customEvent);
|
onLinkOpen(
|
||||||
|
{
|
||||||
|
...element,
|
||||||
|
link: normalizeLink(element.link),
|
||||||
|
},
|
||||||
|
customEvent,
|
||||||
|
);
|
||||||
if (customEvent.defaultPrevented) {
|
if (customEvent.defaultPrevented) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@ -231,21 +238,6 @@ const getCoordsForPopover = (
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeLink = (link: string) => {
|
|
||||||
link = link.trim();
|
|
||||||
if (link) {
|
|
||||||
// prefix with protocol if not fully-qualified
|
|
||||||
if (!link.includes("://") && !/^[[\\/]/.test(link)) {
|
|
||||||
link = `https://${link}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return link;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isLocalLink = (link: string | null) => {
|
|
||||||
return !!(link?.includes(location.origin) || link?.startsWith("/"));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actionLink = register({
|
export const actionLink = register({
|
||||||
name: "hyperlink",
|
name: "hyperlink",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
|
@ -15,6 +15,7 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
|
||||||
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
||||||
- Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
- Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
||||||
|
|
||||||
|
@ -247,3 +247,5 @@ export { WelcomeScreen };
|
|||||||
export { LiveCollaborationTrigger };
|
export { LiveCollaborationTrigger };
|
||||||
|
|
||||||
export { DefaultSidebar } from "../../components/DefaultSidebar";
|
export { DefaultSidebar } from "../../components/DefaultSidebar";
|
||||||
|
|
||||||
|
export { normalizeLink } from "../../data/url";
|
||||||
|
@ -50,6 +50,7 @@ import {
|
|||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { getContainingFrame } from "../frame";
|
import { getContainingFrame } from "../frame";
|
||||||
|
import { normalizeLink } from "../data/url";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@ -1203,7 +1204,7 @@ export const renderElementToSvg = (
|
|||||||
// if the element has a link, create an anchor tag and make that the new root
|
// if the element has a link, create an anchor tag and make that the new root
|
||||||
if (element.link) {
|
if (element.link) {
|
||||||
const anchorTag = svgRoot.ownerDocument!.createElementNS(SVG_NS, "a");
|
const anchorTag = svgRoot.ownerDocument!.createElementNS(SVG_NS, "a");
|
||||||
anchorTag.setAttribute("href", element.link);
|
anchorTag.setAttribute("href", normalizeLink(element.link));
|
||||||
root.appendChild(anchorTag);
|
root.appendChild(anchorTag);
|
||||||
root = anchorTag;
|
root = anchorTag;
|
||||||
}
|
}
|
||||||
|
@ -1086,6 +1086,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
|
"@braintree/sanitize-url@6.0.2":
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
|
||||||
|
integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==
|
||||||
|
|
||||||
"@csstools/normalize.css@*":
|
"@csstools/normalize.css@*":
|
||||||
version "12.0.0"
|
version "12.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4"
|
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user