diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82e409e3..5b95b70d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,13 @@ and this project adheres to
- ✅(export) add PDF regression tests #1762
- 📝(docs) Add language configuration documentation #1757
- 🔒(helm) Set default security context #1750
-- ✨(backend) use langfuse to monitor AI actions
+- ✨(backend) use langfuse to monitor AI actions #1776
+
+### Changed
+
+- ♿(frontend) improve accessibility:
+ - ♿(frontend) make html export accessible to screen reader users #1743
+ - ♿(frontend) add missing label and fix Axes errors to improve a11y #1693
### Fixed
@@ -24,12 +30,7 @@ and this project adheres to
### Security
- 🔒️(backend) validate more strictly url used by cors-proxy endpoint #1768
-
-### Changed
-
-- ♿(frontend) improve accessibility:
- - ♿(frontend) make html export accessible to screen reader users #1743
- - ♿(frontend) add missing label and fix Axes errors to improve a11y #1693
+- 🔒️(frontend) fix props vulnerability in Interlinking #1792
## [4.3.0] - 2026-01-05
diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json
index cb701fc9..a0dcf896 100644
--- a/src/frontend/apps/impress/package.json
+++ b/src/frontend/apps/impress/package.json
@@ -68,6 +68,7 @@
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.6",
+ "uuid": "13.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
"zustand": "5.0.9"
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx
index 5dc9a2be..be5145dd 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx
@@ -1,21 +1,23 @@
-/* eslint-disable react-hooks/rules-of-hooks */
+import {
+ PartialCustomInlineContentFromConfig,
+ StyleSchema,
+} from '@blocknote/core';
import { createReactInlineContentSpec } from '@blocknote/react';
+import * as Sentry from '@sentry/nextjs';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { css } from 'styled-components';
+import { validate as uuidValidate } from 'uuid';
import { BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
-import { getEmojiAndTitle, useDoc } from '@/docs/doc-management';
+import { getEmojiAndTitle, useDoc } from '@/docs/doc-management/';
export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
{
type: 'interlinkingLinkInline',
propSchema: {
- url: {
- default: '',
- },
docId: {
default: '',
},
@@ -27,46 +29,97 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
},
{
render: ({ editor, inlineContent, updateInlineContent }) => {
- const { data: doc } = useDoc({ id: inlineContent.props.docId });
- const isEditable = editor.isEditable;
+ if (!inlineContent.props.docId) {
+ return null;
+ }
/**
- * Update the content title if the referenced doc title changes
+ * Should not happen
*/
- useEffect(() => {
- if (
- isEditable &&
- doc?.title &&
- doc.title !== inlineContent.props.title
- ) {
- updateInlineContent({
- type: 'interlinkingLinkInline',
- props: {
- ...inlineContent.props,
- title: doc.title,
- },
- });
- }
+ if (!uuidValidate(inlineContent.props.docId)) {
+ Sentry.captureException(
+ new Error(`Invalid docId: ${inlineContent.props.docId}`),
+ {
+ extra: { info: 'InterlinkingLinkInlineContent' },
+ },
+ );
- /**
- * ⚠️ When doing collaborative editing, doc?.title might be out of sync
- * causing an infinite loop of updates.
- * To prevent this, we only run this effect when doc?.title changes,
- * not when inlineContent.props.title changes.
- */
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [doc?.title, isEditable]);
+ updateInlineContent({
+ type: 'interlinkingLinkInline',
+ props: {
+ docId: '',
+ title: '',
+ },
+ });
- return ;
+ return null;
+ }
+
+ return (
+
+ );
},
},
);
interface LinkSelectedProps {
- url: string;
+ docId: string;
title: string;
+ isEditable: boolean;
+ updateInlineContent: (
+ update: PartialCustomInlineContentFromConfig<
+ {
+ readonly type: 'interlinkingLinkInline';
+ readonly propSchema: {
+ readonly docId: {
+ readonly default: '';
+ };
+ readonly title: {
+ readonly default: '';
+ };
+ };
+ readonly content: 'none';
+ },
+ StyleSchema
+ >,
+ ) => void;
}
-const LinkSelected = ({ url, title }: LinkSelectedProps) => {
+export const LinkSelected = ({
+ docId,
+ title,
+ isEditable,
+ updateInlineContent,
+}: LinkSelectedProps) => {
+ const { data: doc } = useDoc({ id: docId });
+
+ /**
+ * Update the content title if the referenced doc title changes
+ */
+ useEffect(() => {
+ if (isEditable && doc?.title && doc.title !== title) {
+ updateInlineContent({
+ type: 'interlinkingLinkInline',
+ props: {
+ docId,
+ title: doc.title,
+ },
+ });
+ }
+
+ /**
+ * ⚠️ When doing collaborative editing, doc?.title might be out of sync
+ * causing an infinite loop of updates.
+ * To prevent this, we only run this effect when doc?.title changes,
+ * not when inlineContent.props.title changes.
+ */
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [doc?.title, docId, isEditable]);
+
const { colorsTokens } = useCunninghamTheme();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
@@ -74,7 +127,7 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
- void router.push(url);
+ void router.push(`/docs/${docId}/`);
};
return (
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx
index 5ef163cf..47428e75 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx
@@ -247,7 +247,6 @@ export const SearchPage = ({
{
type: 'interlinkingLinkInline',
props: {
- url: `/docs/${doc.id}`,
docId: doc.id,
title: doc.title || untitledDocument,
},
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx
index afb8c0e7..7da97f08 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkDocx.tsx
@@ -1,16 +1,24 @@
import { ExternalHyperlink, TextRun } from 'docx';
+import { getEmojiAndTitle } from '@/docs/doc-management';
+
import { DocsExporterDocx } from '../types';
export const inlineContentMappingInterlinkingLinkDocx: DocsExporterDocx['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
+ if (!inline.props.docId) {
+ return new TextRun('');
+ }
+
+ const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
+
return new ExternalHyperlink({
children: [
new TextRun({
- text: `📄${inline.props.title}`,
+ text: `${emoji || '📄'}${titleWithoutEmoji}`,
bold: true,
}),
],
- link: window.location.origin + inline.props.url,
+ link: window.location.origin + `/docs/${inline.props.docId}/`,
});
};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkODT.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkODT.tsx
index 4ffadf08..ef9d3d9a 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkODT.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkODT.tsx
@@ -1,11 +1,17 @@
import React from 'react';
+import { getEmojiAndTitle } from '@/docs/doc-management';
+
import { DocsExporterODT } from '../types';
export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
- const url = window.location.origin + inline.props.url;
- const title = inline.props.title;
+ if (!inline.props.docId) {
+ return null;
+ }
+
+ const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
+ const url = window.location.origin + `/docs/${inline.props.docId}/`;
// Create ODT hyperlink using React.createElement to avoid TypeScript JSX namespace issues
// Uses the same structure as BlockNote's default link mapping
@@ -18,6 +24,6 @@ export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings'
xlinkShow: 'replace',
xlinkHref: url,
},
- `📄${title}`,
+ `${emoji || '📄'}${titleWithoutEmoji}`,
);
};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx
index c2d204b7..87165dfa 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/interlinkingLinkPDF.tsx
@@ -1,21 +1,29 @@
import { Image, Link, Text } from '@react-pdf/renderer';
+import { getEmojiAndTitle } from '@/docs/doc-management';
+
import DocSelectedIcon from '../assets/doc-selected.png';
import { DocsExporterPDF } from '../types';
export const inlineContentMappingInterlinkingLinkPDF: DocsExporterPDF['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
+ if (!inline.props.docId) {
+ return <>>;
+ }
+
+ const { emoji, titleWithoutEmoji } = getEmojiAndTitle(inline.props.title);
+
return (
{' '}
- {' '}
- {inline.props.title}{' '}
+ {emoji || }{' '}
+ {titleWithoutEmoji}{' '}
);
};