Compare commits

...

2 Commits

Author SHA1 Message Date
dwelle
dd27e48c13 docs: release @excalidraw/excalidraw@0.15.3 2023-08-16 15:17:47 +02:00
Christopher Chedeau
139b901ff1 fix: stronger enforcement of normalizeLink (#6728)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-08-16 15:14:55 +02:00
11 changed files with 75 additions and 25 deletions

View File

@ -20,6 +20,7 @@
}, },
"dependencies": { "dependencies": {
"@dwelle/tunnel-rat": "0.1.1", "@dwelle/tunnel-rat": "0.1.1",
"@braintree/sanitize-url": "6.0.2",
"@sentry/browser": "6.2.5", "@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5", "@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",

View File

@ -279,13 +279,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 { Fonts } from "../scene/Fonts"; import { Fonts } from "../scene/Fonts";
import { actionPaste } from "../actions/actionClipboard"; import { actionPaste } from "../actions/actionClipboard";
@ -2947,12 +2946,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";
@ -2960,7 +2966,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;
} }
} }
} }

View File

@ -40,6 +40,7 @@ import {
getDefaultLineHeight, getDefaultLineHeight,
measureBaseline, measureBaseline,
} from "../element/textElement"; } from "../element/textElement";
import { normalizeLink } from "./url";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@ -139,7 +140,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
View 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
View 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("/"));
};

View File

@ -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) => {

View File

@ -11,6 +11,10 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## 0.15.3 (2023-08-16)
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
## 0.15.2 (2023-04-20) ## 0.15.2 (2023-04-20)
### Docs ### Docs

View File

@ -245,3 +245,4 @@ export { MainMenu };
export { useDevice } from "../../components/App"; export { useDevice } from "../../components/App";
export { WelcomeScreen }; export { WelcomeScreen };
export { LiveCollaborationTrigger }; export { LiveCollaborationTrigger };
export { normalizeLink } from "../../data/url";

View File

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/excalidraw", "name": "@excalidraw/excalidraw",
"version": "0.15.2", "version": "0.15.3",
"main": "main.js", "main": "main.js",
"types": "types/packages/excalidraw/index.d.ts", "types": "types/packages/excalidraw/index.d.ts",
"files": [ "files": [

View File

@ -48,6 +48,7 @@ import {
getMaxContainerWidth, getMaxContainerWidth,
} from "../element/textElement"; } from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
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
@ -1140,7 +1141,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;
} }

View File

@ -1326,6 +1326,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"