Skip to content

Sanitize href props with xss vulnerability V2 #1000

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

Merged
merged 11 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,257 changes: 3,602 additions & 1,655 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.1",
"@testing-library/user-event": "^13.2.1",
Expand All @@ -56,6 +57,7 @@
"webpack-dev-server": "^4.7.4"
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.0",
"@plotly/dash-component-plugins": "^1.2.0",
"classnames": "^2.2.6",
"fast-isnumeric": "^1.1.3",
Expand Down
7 changes: 5 additions & 2 deletions src/components/badge/Badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {omit} from 'ramda';
import RBBadge from 'react-bootstrap/Badge';
import Link from '../../private/Link';
import {bootstrapColors} from '../../private/BootstrapColors';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* Badges can be used to add counts or labels to other components.
Expand All @@ -22,6 +23,8 @@ const Badge = props => {
...otherProps
} = props;

const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);

const incrementClicks = () => {
if (setProps) {
setProps({
Expand All @@ -36,8 +39,8 @@ const Badge = props => {

return (
<RBBadge
as={href && Link}
href={href}
as={sanitizedUrl && Link}
href={sanitizedUrl}
bg={isBootstrapColor ? color : null}
text={text_color}
className={class_name || className}
Expand Down
36 changes: 28 additions & 8 deletions src/components/breadcrumb/Breadcrumb.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,33 @@ import PropTypes from 'prop-types';
import RBBreadcrumb from 'react-bootstrap/Breadcrumb';

import Link from '../../private/Link';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* Use breadcrumbs to create a navigation breadcrumb in your app.
*/

const BreadcrumbItem = ({
href,
setProps,
external_link,
label,
...otherProps
}) => {
const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);

return (
<RBBreadcrumb.Item
linkAs={sanitizedUrl && Link}
href={sanitizedUrl}
linkProps={sanitizedUrl && {external_link}}
{...otherProps}
>
{label}
</RBBreadcrumb.Item>
);
};

const Breadcrumb = ({
items,
tag,
Expand All @@ -16,6 +39,7 @@ const Breadcrumb = ({
item_class_name,
itemClassName,
item_style,
setProps,
...otherProps
}) => (
<RBBreadcrumb
Expand All @@ -27,16 +51,12 @@ const Breadcrumb = ({
{...otherProps}
>
{(items || []).map((item, idx) => (
<RBBreadcrumb.Item
<BreadcrumbItem
key={`${item.value}${idx}`}
active={item.active}
linkAs={item.href && Link}
className={item_class_name || itemClassName}
href={item.href}
linkProps={item.href && {external_link: item.external_link}}
>
{item.label}
</RBBreadcrumb.Item>
setProps={setProps}
{...item}
/>
))}
</RBBreadcrumb>
);
Expand Down
7 changes: 5 additions & 2 deletions src/components/button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {omit} from 'ramda';
import RBButton from 'react-bootstrap/Button';
import Link from '../../private/Link';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* A component for creating Bootstrap buttons with different style options. The
Expand Down Expand Up @@ -34,6 +35,8 @@ const Button = props => {
...otherProps
} = props;

const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);

const incrementClicks = () => {
if (!disabled && setProps) {
setProps({
Expand All @@ -42,7 +45,7 @@ const Button = props => {
});
}
};
const useLink = href && !disabled;
const useLink = sanitizedUrl && !disabled;
otherProps[useLink ? 'preOnClick' : 'onClick'] = onClick || incrementClicks;

if (useLink) {
Expand All @@ -56,7 +59,7 @@ const Button = props => {
as={useLink ? Link : 'button'}
variant={outline ? `outline-${color}` : color}
type={useLink ? undefined : type}
href={disabled ? undefined : href}
href={disabled ? undefined : sanitizedUrl}
disabled={disabled}
download={useLink ? download : undefined}
name={useLink ? undefined : name}
Expand Down
12 changes: 9 additions & 3 deletions src/components/card/CardLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {omit} from 'ramda';
import RBCard from 'react-bootstrap/Card';
import Link from '../../private/Link';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* Use card link to add consistently styled links to your cards. Links can be
Expand All @@ -15,12 +16,16 @@ const CardLink = props => {
disabled,
className,
class_name,
href,
setProps,
...otherProps
} = props;

const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);

const incrementClicks = () => {
if (!disabled && props.setProps) {
props.setProps({
if (!disabled && setProps) {
setProps({
n_clicks: props.n_clicks + 1,
n_clicks_timestamp: Date.now()
});
Expand All @@ -35,8 +40,9 @@ const CardLink = props => {
as={Link}
preOnClick={incrementClicks}
disabled={disabled}
href={sanitizedUrl}
className={class_name || className}
{...omit(['setProps', 'n_clicks', 'n_clicks_timestamp'], otherProps)}
{...omit(['n_clicks', 'n_clicks_timestamp'], otherProps)}
>
{children}
</RBCard.Link>
Expand Down
7 changes: 5 additions & 2 deletions src/components/dropdownmenu/DropdownMenuItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import RBDropdown from 'react-bootstrap/Dropdown';

import Link from '../../private/Link';
import {DropdownMenuContext} from '../../private/DropdownMenuContext';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* Use DropdownMenuItem to build up the content of a DropdownMenu.
Expand All @@ -26,6 +27,8 @@ const DropdownMenuItem = props => {
...otherProps
} = props;

const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);

const context = useContext(DropdownMenuContext);

const handleClick = e => {
Expand All @@ -40,7 +43,7 @@ const DropdownMenuItem = props => {
}
};

const useLink = href && !disabled;
const useLink = sanitizedUrl && !disabled;
otherProps[useLink ? 'preOnClick' : 'onClick'] = e => handleClick(e);

if (header) {
Expand All @@ -52,7 +55,7 @@ const DropdownMenuItem = props => {
return (
<RBDropdown.Item
as={useLink ? Link : 'button'}
href={useLink ? href : undefined}
href={useLink ? sanitizedUrl : undefined}
disabled={disabled}
target={useLink ? target : undefined}
className={class_name || className}
Expand Down
7 changes: 5 additions & 2 deletions src/components/listgroup/ListGroupItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {omit} from 'ramda';
import RBListGroupItem from 'react-bootstrap/ListGroupItem';
import Link from '../../private/Link';
import {bootstrapColors} from '../../private/BootstrapColors';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* Create a single item in a `ListGroup`.
Expand All @@ -24,6 +25,8 @@ const ListGroupItem = props => {
...otherProps
} = props;

const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);

const incrementClicks = () => {
if (!disabled && setProps) {
setProps({
Expand All @@ -33,13 +36,13 @@ const ListGroupItem = props => {
}
};
const isBootstrapColor = bootstrapColors.has(color);
const useLink = href && !disabled;
const useLink = sanitizedUrl && !disabled;
otherProps[useLink ? 'preOnClick' : 'onClick'] = incrementClicks;

return (
<RBListGroupItem
as={useLink ? Link : 'li'}
href={href}
href={sanitizedUrl}
target={useLink ? target : undefined}
disabled={disabled}
variant={isBootstrapColor ? color : null}
Expand Down
9 changes: 6 additions & 3 deletions src/components/nav/NavLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {omit} from 'ramda';
import classNames from 'classnames';
import {History} from '@plotly/dash-component-plugins';
import Link from '../../private/Link';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* Add a link to a `Nav`. Can be used as a child of `NavItem` or of `Nav`
Expand All @@ -24,11 +25,13 @@ const NavLink = props => {
...otherProps
} = props;

const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);

const pathnameToActive = pathname => {
setLinkActive(
active === true ||
(active === 'exact' && pathname === href) ||
(active === 'partial' && pathname.startsWith(href))
(active === 'exact' && pathname === sanitizedUrl) ||
(active === 'partial' && pathname.startsWith(sanitizedUrl))
);
};

Expand Down Expand Up @@ -61,7 +64,7 @@ const NavLink = props => {
className={classes}
disabled={disabled}
preOnClick={incrementClicks}
href={href}
href={sanitizedUrl}
{...omit(['n_clicks_timestamp'], otherProps)}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
Expand Down
17 changes: 14 additions & 3 deletions src/components/nav/NavbarBrand.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@ import PropTypes from 'prop-types';
import {omit} from 'ramda';
import RBNavbarBrand from 'react-bootstrap/NavbarBrand';
import Link from '../../private/Link';
import {sanitizeAndCheckUrl} from '../../private/util';

/**
* Call out attention to a brand name or site title within a navbar.
*/
const NavbarBrand = props => {
const {children, loading_state, className, class_name, ...otherProps} = props;
const {
children,
loading_state,
className,
class_name,
href,
setProps,
...otherProps
} = props;
const sanitizedUrl = sanitizeAndCheckUrl(href, setProps);
return (
<RBNavbarBrand
className={class_name || className}
{...omit(['setProps'], otherProps)}
as={props.href ? Link : 'span'}
{...otherProps}
as={sanitizedUrl ? Link : 'span'}
href={sanitizedUrl}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
Expand Down
7 changes: 6 additions & 1 deletion src/components/nav/NavbarSimple.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {omit} from 'ramda';
import RBNavbar from 'react-bootstrap/Navbar';
import RBContainer from 'react-bootstrap/Container';
import {bootstrapColors} from '../../private/BootstrapColors';
import {sanitizeAndCheckUrl} from '../../private/util';

import Nav from './Nav';
import NavbarBrand from './NavbarBrand';
Expand All @@ -29,8 +30,12 @@ const NavbarSimple = props => {
loading_state,
className,
class_name,
setProps,
...otherProps
} = props;

const sanitizedUrl = sanitizeAndCheckUrl(brand_href, setProps);

const isBootstrapColor = bootstrapColors.has(color);

const [navbarOpen, setNavbarOpen] = useState(false);
Expand All @@ -52,7 +57,7 @@ const NavbarSimple = props => {
<RBContainer fluid={fluid}>
{brand && (
<NavbarBrand
href={brand_href}
href={sanitizedUrl}
style={brand_style}
external_link={brand_external_link}
>
Expand Down
26 changes: 25 additions & 1 deletion src/private/util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {type} from 'ramda';
import React, {useEffect, useMemo} from 'react';
import {sanitizeUrl} from '@braintree/sanitize-url';

const parseChildrenToArray = children => {
if (children && !Array.isArray(children)) {
Expand Down Expand Up @@ -73,4 +75,26 @@ const stringifyId = id => {
return '{' + parts.join(',') + '}';
};

export {parseChildrenToArray, resolveChildProps, sanitizeOptions, stringifyId};
const sanitizeAndCheckUrl = (href, setProps) => {
const sanitizedUrl = useMemo(() => {
return href ? sanitizeUrl(href) : undefined;
}, [href]);

useEffect(() => {
if (sanitizedUrl && sanitizedUrl !== href) {
setProps({
_dash_error: new Error(`Dangerous link detected:: ${href}`)
});
}
}, [href, sanitizedUrl]);

return sanitizedUrl;
};

export {
parseChildrenToArray,
resolveChildProps,
sanitizeOptions,
stringifyId,
sanitizeAndCheckUrl
};
Loading