A security-focused wrapper for react-markdown that filters URLs based on allowed prefixes
npm install harden-react-markdownA wrapper for react-markdown that ensures that untrusted
markdown does not contain images from and links to unexpected origins.
This is particularly important for markdown returned from LLMs in AI agents which might have been subject to prompt
injection.
This package validates URL prefixes and URL origins. Prefix allow-lists can be circumvented
with open redirects, so make sure to make the prefixes are specific enough to avoid such attacks.
E.g. it is more secure to allow https://example.com/images/ than it is to allow all ofhttps://example.com/ which may contain open redirects.
Additionally, URLs may contain path traversal like /../. This package does not resolve these.
It is your responsibility that your web server does not allow such traversal.
- 🔒 URL Filtering: Blocks links and images that don't match allowed URL prefixes
- 🔧 Drop-in Replacement: Works with any react-markdown compatible component
``bash`
npm install harden-react-markdown react react-markdownor
yarn add harden-react-markdown react react-markdownor
pnpm add harden-react-markdown react react-markdown
`tsx
import React from "react";
import ReactMarkdown from "react-markdown";
import hardenReactMarkdown from "harden-react-markdown";
// Create a hardened version of ReactMarkdown
const HardenedMarkdown = hardenReactMarkdown(ReactMarkdown);
function MyComponent() {
const markdown =
; return (
defaultOrigin="https://mysite.com"
allowedLinkPrefixes={["https://github.com/", "https://docs."]}
allowedImagePrefixes={["https://via.placeholder.com/", "/"]}
>
{markdown}
);
}
`API
$3
Creates a hardened version of any react-markdown compatible component.
#### Parameters
-
MarkdownComponent: A React component that accepts Options from react-markdown#### Returns
A new component with enhanced security that accepts all original props plus:
$3
####
defaultOrigin?: string- The origin to resolve relative URLs against
- Required when
allowedLinkPrefixes or allowedImagePrefixes are provided
- Example: "https://mysite.com"####
allowedLinkPrefixes?: string[]- Array of URL prefixes that are allowed for links
- Links not matching these prefixes will be blocked and shown as
[blocked]
- Use "*" to allow all URLs (disables filtering. However, javascript: and data: URLs are always disallowed)
- Default: [] (blocks all links)
- Example: ['https://github.com/', 'https://docs.example.com/'] or ['*']####
allowedImagePrefixes?: string[]- Array of URL prefixes that are allowed for images
- Images not matching these prefixes will be blocked and shown as placeholders
- Use
"*" to allow all URLs (disables filtering. However, javascript: and data: URLs are always disallowed unless allowDataImages is enabled)
- Default: [] (blocks all images)
- Example: ['https://via.placeholder.com/', '/'] or ['*']####
allowDataImages?: boolean- When set to
true, allows data:image/* URLs (base64-encoded images) in image sources
- This is useful for scenarios where images are embedded directly in markdown (e.g., documents converted from PDF or .docx)
- Only data:image/* URLs are allowed; other data: URLs (like data:text/html) remain blocked for security
- data: URLs are never allowed in links, regardless of this setting
- Default: false (blocks all data: URLs)
- Example: true####
allowedProtocols?: string[]- Array of custom URL protocols that are allowed in links
- Useful for deep links to applications (e.g.,
tel:, mailto:, postman:, vscode:, slack:)
- Use "*" to allow all protocols that can be parsed as valid URLs
- Dangerous protocols (javascript:, data:, file:, vbscript:) are always blocked regardless of this setting
- Default: [] (only allows built-in safe protocols: https:, http:, mailto:, irc:, ircs:, xmpp:, blob:)
- Example: ['tel:', 'postman:', 'vscode:'] or ['*']All other props are passed through to the wrapped markdown component.
Examples
$3
`tsx
const HardenedMarkdown = hardenReactMarkdown(ReactMarkdown);// Blocks all external links and images by default
{markdownContent} ;
`$3
`tsx
defaultOrigin="https://mysite.com"
allowedLinkPrefixes={[
"https://github.com/",
"https://docs.github.com/",
"https://www.npmjs.com/",
]}
allowedImagePrefixes={[
"https://via.placeholder.com/",
"https://images.unsplash.com/",
"/", // Allow relative images
]}
>
{markdownContent}
`$3
`tsx
defaultOrigin="https://mysite.com"
allowedLinkPrefixes={["https://mysite.com/"]}
allowedImagePrefixes={["https://mysite.com/"]}
>
{}
`$3
`tsx
"]} allowedImagePrefixes={[""]}>
{ }
`Note: Using
"*" disables URL filtering entirely. Only use this when you trust the markdown source.$3
`tsx
defaultOrigin="https://mysite.com"
allowedImagePrefixes={["https://mysite.com/"]}
allowDataImages={true}
>
{}
`Note: This is particularly useful when converting documents from formats like PDF or .docx where images are embedded as base64. Only
data:image/* URLs are allowed; other data: URLs remain blocked for security.$3
Enable custom protocols for deep linking to applications and services:
`tsx
{}
`Common use cases:
-
tel: - Phone number links that open the dialer on mobile devices
- mailto: - Email links (allowed by default, but shown here for completeness)
- sms: - SMS/text message links
- postman:, vscode:, slack: - Deep links to desktop applications
- Custom app protocols - Links to your own Electron or native applicationsYou can also use the wildcard to allow any custom protocol:
`tsx
{Custom Protocol Link}
`Security Note: Even with
allowedProtocols={["*"]}, dangerous protocols like javascript:, data:, file:, and vbscript: are always blocked for security. Custom protocols are safe because they trigger OS-level protocol handlers and don't execute in the browser context.$3
`tsx
const CustomMarkdown = (props) => (
);const HardenedCustomMarkdown = hardenReactMarkdown(CustomMarkdown);
defaultOrigin="https://mysite.com"
allowedLinkPrefixes={["https://trusted.com/"]}
>
{markdownContent}
;
`Security Features
$3
- Links: Filters
href attributes in elements
- Images: Filters src attributes in elements
- Relative URLs: Properly resolves and validates relative URLs against defaultOrigin
- Path Traversal Protection: Normalizes URLs to prevent ../ attacks
- Wildcard Support: Use "*" prefix to disable filtering (only when markdown is trusted)
- Prefix Matching: Validates that URLs start with allowed prefixes and have matching origins$3
- Blocked Links: Rendered as plain text with
[blocked] indicator
- Blocked Images: Rendered as placeholder text with image description
- User Feedback: Clear indication when content has been blocked for security$3
- XSS Prevention: Blocks
javascript:, data:, vbscript:, file: and other dangerous protocols (always, regardless of configuration)
- Redirect Protection: Prevents unauthorized redirects to malicious sites
- Tracking Prevention: Blocks unauthorized image tracking pixels
- Domain Spoofing: Validates full URLs, not just domains
- Custom Protocols: Optional support for custom protocols (e.g., tel:, postman:, vscode:) with explicit opt-in via allowedProtocolsTypeScript Support
Full TypeScript support with strict type checking:
`tsx
// Type-safe component creation
const HardenedMarkdown = hardenReactMarkdown(ReactMarkdown);// Inferred prop types include both react-markdown Options and security options
type Props = Parameters[0];
// Works with custom markdown components
const CustomMarkdown = (props: Options & { customProp?: string }) => (
);
const HardenedCustom = hardenReactMarkdown(CustomMarkdown);
// Props now include customProp + security options
`Testing
The package includes comprehensive tests covering:
- Basic markdown rendering
- URL filtering for links and images
- Relative URL handling
- Security bypass prevention
- Edge cases and malformed URLs
- TypeScript type safety
Run tests:
`bash
npm test
`Contributing
1. Fork the repository
2. Create your feature branch (
git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add some amazing feature')
4. Push to the branch (git push origin feature/amazing-feature`)MIT License - see the LICENSE file for details.
If you discover a security vulnerability, please send an e-mail to