-
Notifications
You must be signed in to change notification settings - Fork 126
Fix: Allow advanced markdown syntax in MarkdownRenderer (fixes #1912) #1935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,18 @@ interface MarkdownRendererProps { | |
| const remarkPlugins: PluggableList = [remarkGfm]; | ||
| const strictSchema = { | ||
| ...defaultSchema, | ||
| tagNames: [ | ||
| ...(defaultSchema.tagNames || []), | ||
| 'input', | ||
| 'table', 'thead', 'tbody', 'tr', 'th', 'td' | ||
| ], | ||
| attributes: { | ||
| ...defaultSchema.attributes, | ||
| '*': ['className', 'style'], | ||
| a: ['href', 'title', 'target', 'rel'], | ||
| input: ['type', 'checked', 'disabled'], | ||
| th: ['align', 'colSpan', 'rowSpan'], | ||
| td: ['align', 'colSpan', 'rowSpan'], | ||
|
Comment on lines
20
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|MarkdownRenderer\.tsx|ChatMessage\.tsx|ChatBox\.tsx)$' || true
echo
echo "== package versions =="
for f in package.json package-lock.json yarn.lock pnpm-lock.yaml; do
if [ -f "$f" ]; then
echo "--- $f ---"
if [ "$f" = "package.json" ]; then
node -e "const p=require('./package.json'); console.log(JSON.stringify({dependencies:p.dependencies,devDependencies:p.devDependencies,engines:p.engines}, null, 2))"
elif [ "$f" = "package-lock.json" ]; then
node -e "const p=require('./package-lock.json'); for (const key of ['node_modules/rehype-sanitize','node_modules/hast-util-sanitize','node_modules/remark-gfm','node_modules/react-markdown']) console.log(key, p.packages?.[key]?.version)"
else
rg -n "rehype-sanitize|hast-util-sanitize|remark-gfm|react-markdown" "$f" -C 2 || true
fi
fi
done
echo
echo "== MarkdownRenderer =="
fd -a 'MarkdownRenderer\.tsx$' . | while read -r f; do
echo "--- $f ---"
sed -n '1,180p' "$f" | cat -n
done
echo
echo "== usages =="
for f in $(fd 'ChatMessage\.tsx$|ChatBox\.tsx$|MarkdownRenderer\.tsx$' .); do
echo "--- $f ---"
wc -l "$f"
sed -n '1,220p' "$f" | cat -n
doneRepository: durdana3105/peer-learning Length of output: 14265 🌐 Web query:
💡 Result: In rehype-sanitize 6.0.0, which utilizes hast-util-sanitize 5.0.0, the default schema includes sanitization logic that affects GFM (GitHub Flavored Markdown) features like footnotes, tables, and IDs [1][2][3]. The core issue involves DOM clobbering prevention [4]. By default, rehype-sanitize and hast-util-sanitize rewrite specific attributes (such as id and name) to prefix them, which is intended to protect against security vulnerabilities [5][6][4]. Specifically regarding GFM footnotes and internal links: - The default schema's clobbering protection often causes mismatches between footnote references and their corresponding footnote definitions because the IDs and the referenced fragments in href attributes are rewritten inconsistently or clobbered [5][6][4]. - While the default schema allows for elements used in GFM tables (such as table, thead, tbody, tr, td, th), the aggressive sanitization of IDs and link targets can still break navigation within those tables or footnoted content [4][7]. If you are encountering broken GFM footnote links or table-related ID issues, it is typically because the library is sanitizing these identifiers by default to mitigate DOM clobbering [4]. You can address this by providing a custom schema to rehype-sanitize, extending the default schema, or adjusting the clobbering configuration if necessary [8][4]. Developers often work around this by either disabling specific clobbering rules or adjusting the schema to allow the required attributes and values without the default prefixing [4]. Citations:
🌐 Web query:
💡 Result: The rehype-sanitize version 6.0.0 package relies on hast-util-sanitize as its underlying utility for cleaning HAST (HTML Abstract Syntax Tree) nodes [1][2]. In the package.json for rehype-sanitize v6.0.0, hast-util-sanitize is listed as a dependency, specifically requiring version ^5.0.0 [1][3]. Rehype-sanitize functions as a higher-level abstraction, utilizing hast-util-sanitize to perform the actual sanitization of the syntax trees while simplifying the integration for users within the unified/rehype ecosystem [1][4][2]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
cd "$tmp"
echo "== fetch packages =="
npm pack github:syntax-tree/hast-util-sanitize@5.0.2 --silent 2>/dev/null || true
tar -xzf hast-util-sanitize-5.0.2.tgz
echo
echo "== locate schema assets =="
find package -maxdepth 3 -type f \( -name '*.js' -o -name '*.mjs' -o -name '*.d.ts' -o -name 'schema.*' \) -print | sort | sed -n '1,120p'
echo
echo "== search default attributes for relevant keys =="
rg -n "default|attributes|footnote|aria|role|scope|headers|clobber|className|data-footnote|id:" package -S || true
echo
echo "== inspect likely default/schema file slices =="
for f in $(find package -maxdepth 3 -type f \( -name '*.js' -o -name '*.mjs' -o -name 'schema.*' \) | sort); do
if rg -q "defaultSchema|attributes|footnote|role|scope|headers" "$f"; then
echo "--- $f ---"
wc -l "$f"
sed -n '1,260p' "$f" | cat -n
fi
doneRepository: durdana3105/peer-learning Length of output: 389 🌐 Web query:
💡 Result: In hast-util-sanitize version 5.0.2, the default schema is exported as defaultSchema [1][2]. It follows GitHub-style sanitation rules to determine which nodes and properties are permitted [1][3][2]. The attributes configuration is defined as a mapping of tag names to allowed property names, where defaultSchema.attributes provides the default values [1][4]. Key characteristics of this configuration include: 1. Special Wildcard Key: The key '' is used to define property names that are allowed on all elements [1][5]. 2. Data Properties: The special value 'data' can be used as a property name to allow all data attributes [1][2]. 3. Granular Control: You can specify simple string arrays for property names, or use nested arrays to restrict specific values (e.g., input: [['type', 'checkbox', 'radio']]) [1][5]. 4. Extending the Schema: To modify the default attributes, you can clone defaultSchema and override the attributes key in your custom configuration [1][4]. If a top-level key is missing in a user-provided schema, the library automatically falls back to the corresponding value from defaultSchema [1][2]. For the full list of default allowed attributes, you can access the defaultSchema object directly in your code: import {defaultSchema} from 'hast-util-sanitize' console.log(defaultSchema.attributes) Citations:
🌐 Web query:
💡 Result: In the hast-util-sanitize package version 5.0.2, there is no file named default.js [1]. The package exports an identifier called defaultSchema, which is defined within the library's source code (typically in lib/index.js or similar) and made available to users [2][3][4]. The defaultSchema serves as the standard configuration for the sanitize function, designed to mimic GitHub's sanitization style [2][3]. It specifies which HTML nodes, attributes, and properties are permitted [5][4]. Because the package uses named exports, you do not import a file named default.js; instead, you import the schema directly from the package [2][3]: import { defaultSchema, sanitize } from 'hast-util-sanitize'; Users can access, inspect, or extend this schema by cloning it or using tools like deepmerge to modify it for their specific requirements [2][5][4]. The package documentation provides examples on how to utilize this default configuration when sanitizing HAST (Hypertext Abstract Syntax Tree) nodes [2][4]. Citations:
🌐 Web query:
💡 Result: The Citations:
🌐 Web query:
💡 Result: In hast-util-sanitize v5.0.2, the default schema ( Citations:
🌐 Web query:
💡 Result: The hast-util-sanitize package, version 5.0.2, uses a default schema (defaultSchema) designed to mimic GitHub's sanitization logic [1][2]. This schema determines which HTML tags and attributes are permitted in the output tree [3][4]. Regarding the specific properties and tags mentioned: 1. className: By default, the schema includes specific rules for the className attribute. While className is not allowed on all elements by default, you can allow it globally by modifying the schema [1][3]. This is commonly done by merging a custom configuration with the defaultSchema [1][2]: import deepmerge from 'deepmerge'; import { defaultSchema } from 'hast-util-sanitize'; const schema = deepmerge(defaultSchema, {attributes: {'*': ['className']}}); 2. table, th, td, footnote (note: footnote is not a standard HTML element), role, scope, headers: The defaultSchema explicitly defines which attributes are permitted for specific elements, including table-related tags [3][5]. - Attributes like scope and headers are typically managed within the attributes configuration for table elements (th/td) [3][6]. - The role attribute is generally stripped by default unless explicitly allowed in your custom schema, as it is often restricted to prevent accessibility-related XSS or spoofing issues. - If an element or attribute is not included in the defaultSchema for a specific tag, it is removed during sanitization [4][5]. To allow these, you must extend the defaultSchema by adding them to the attributes object for the corresponding tag names [1][7]. The package does not have a single flat file that lists all allowed attributes; instead, it uses a structured schema object [4]. You can inspect the actual allowed values by importing defaultSchema from the package in a Node.js environment to view the full default configuration [1][8]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
cd "$tmp"
echo "== fetch unrejected package tarball if possible =="
url="$(node - <<'JS'
const fs = require('fs')
const lock = JSON.parse(fs.readFileSync('../package-lock.json', 'utf8'))
const pkg = lock.packages?.['node_modules/hast-util-sanitize']?.resolved || lock.packages?.['node_modules/hast-util-sanitize']?.version
console.log(pkg)
JS
)"
echo "resolved: $url"
npm fetch "$url" --silent > hast-util-sanitize.tgz 2>&1 || cat hast-util-sanitize.tgz
tar -tzf hast-util-sanitize.tgz | sed -n '1,120p'
tar -xzf hast-util-sanitize.tgz
echo
echo "== default schema source =="
find package -maxdepth 3 -type f \( -name '*.js' -o -name '*.mjs' -o -name '*.d.ts' \) -print0 | sort -z | while IFS= read -r -d '' f; do
if grep -qE "defaultSchema|attributes|className|aria|role|scope|headers|footnote|data-footnote|clobber" "$f"; then
echo "--- $f ---"
wc -l "$f"
grep -nE "defaultSchema|attributes|className|aria|role|scope|headers|footnote|data-footnote|clobber|protocols" "$f" -C 3 || true
fi
done
echo
echo "== lockfile exact package info =="
node - <<'JS'
const fs = require('fs')
const lock = JSON.parse(fs.readFileSync('../../package-lock.json', 'utf8'))
for (const key of ['node_modules/react-markdown','node_modules/remark-gfm','node_modules/rehype-sanitize','node_modules/hast-util-sanitize']) {
console.log(key, lock.packages?.[key])
}
JSRepository: durdana3105/peer-learning Length of output: 981 🏁 Script executed: #!/bin/bash
set -euo pipefail
cd "$(pwd)"
tmp="$(mktemp -d)"
cd "$tmp"
echo "== inspect local lock package info =="
node - <<'JS'
const fs = require('fs')
const lock = JSON.parse(fs.readFileSync('/workspace/package-lock.json', 'utf8'))
for (const key of ['node_modules/react-md','node_modules/react-markdown','node_modules/remark-gfm','node_modules/rehype-sanitize','node_modules/hast-util-sanitize']) {
console.log(key, JSON.stringify(lock.packages?.[key], null, 2))
}
JS
echo
echo "== fetch hast-util-sanitize lock resolved source if resolvable =="
node - <<'JS'
const fs = require('fs')
const lock = JSON.parse(fs.readFileSync('/workspace/package-lock.json', 'utf8'))
const pkg = lock.packages?.['node_modules/hast-util-sanitize']
console.log(JSON.stringify(pkg, null, 2))
JSRepository: durdana3105/peer-learning Length of output: 984 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== package files and cwd =="
pwd
git ls-files 'package-lock.json' 'package.json' | cat -n
echo
echo "== local lock package info =="
node - <<'JS'
const fs = require('fs')
const paths = ['package-lock.json', 'package-lock.json', '../package-lock.json', '/workspace/package-lock.json']
let lock
for (const p of paths) {
try {
if (fs.existsSync(p)) {
lock = JSON.parse(fs.readFileSync(p, 'utf8'))
console.log('loaded', p)
break
}
} catch (e) { console.error(p, e.message) }
}
if (!lock) throw new Error('no package-lock.json loaded')
for (const key of ['node_modules/react-markdown','node_modules/remark-gfm','node_modules/rehype-sanitize','node_modules/hast-util-sanitize']) {
console.log(key)
console.log(JSON.stringify(lock.packages?.[key], null, 2))
}
JSRepository: durdana3105/peer-learning Length of output: 2685 🌐 Web query:
💡 Result: In the npm package hast-util-sanitize version 5.0.2, the source code is located within the lib directory, and it exports an identifier named defaultSchema [1][2][3]. This defaultSchema provides a predefined configuration for the sanitize function, which is designed to mirror how GitHub handles HTML sanitization [4][5]. Users can customize sanitization by providing their own schema or by extending the defaultSchema [4][5]. The package is written as an ESM-only module and is maintained by the syntax-tree collective [1][3]. You can view the package contents and source structure directly via services like UNPKG [6]. Citations:
🌐 Web query:
💡 Result: In hast-util-sanitize v5.0.2, the default configuration is determined by the defaultSchema object, which is modeled after GitHub's sanitation practices [1][2]. Regarding the specific attributes you inquired about: 1. className: This property is generally allowed on elements if explicitly configured, but it is not included in the default schema's wildcard ('') list by default [1][3]. To allow className on all elements, you must merge a custom schema with the defaultSchema, for example: deepmerge(defaultSchema, {attributes: {'': ['className']}}) [1][4][3]. 2. role: ARIA attributes (such as role) are typically allowed by the default schema for accessibility, as they are part of the standard set of attributes deemed safe by GitHub-style sanitation [5]. 3. scope and headers: These attributes are standard for HTML table elements and are included within the defaultSchema for relevant tags (such as th) to maintain valid, accessible table structures [5]. 4. table and footnote: These refer to HTML tags rather than attributes. The defaultSchema permits standard HTML elements like table (and associated tags like thead, tbody, tr, th, td), provided they do not contain dangerous properties or nested structures [1][3]. A footnote is not a native HTML tag and is not explicitly handled by the default schema; if you use custom tags for footnotes (e.g., ), they will be stripped or sanitized unless you add them to the allowed tagNames in your schema [2]. To modify these behaviors, you can pass a custom schema to the sanitize function. If any top-level key (such as attributes) is missing in your provided schema, the corresponding value from defaultSchema is used automatically [1][3]. Citations:
Merge the The nested object spread replaces each schema entry instead of extending it. This keeps 🤖 Prompt for AI Agents |
||
| }, | ||
| protocols: { | ||
| ...defaultSchema.protocols, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: durdana3105/peer-learning
Length of output: 26834
🌐 Web query:
rehype-sanitize v6 inline-style-parser style sanitizer defaultSchema allows style property💡 Result:
In rehype-sanitize v6, the defaultSchema does not allow the style property [1][2][3]. The library is designed to be restrictive by default to prevent security risks such as CSS-based attacks (e.g., overlaying invisible elements or data exfiltration via external resources) [3]. If your application requires support for inline styles, you must explicitly extend the defaultSchema by adding the style attribute to the elements where you want to permit it [3]. Because rehype-sanitize uses inline-style-parser to handle style strings, providing access to the style attribute allows the plugin to parse and sanitize the content within that attribute rather than stripping it entirely [4][5][6]. To allow the style attribute, you can configure the plugin as follows [3]: import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; const customSchema = {...defaultSchema, attributes: {...defaultSchema.attributes, // Add 'style' to the allowed attributes for the desired elements span: [...(defaultSchema.attributes?.span || []), 'style'], }, }; // Use the custom schema in your unified pipeline //.use(rehypeSanitize, customSchema) When enabling this, it is recommended to be deliberate about which elements you allow the style attribute on, as it increases the attack surface of your application [3].
Citations:
Security Misconfiguration (CWE-20): Improper Input Validation
Remove the global
styleallowance unless inline styling is required.Adding
styletoattributes["*"]lets user/assistant markdown pass inline styles throughrehypeSanitize.inline-style-parsersanitizes those values, but this still extends the attack surface and can throw on invalid CSS. Usestyleonly on specific elements when needed; GFM tables should rely onalignattributes instead.🤖 Prompt for AI Agents