Skip to content

Sanitize href props with xss vulnerability #999

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

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"ramda": "^0.27.1",
"react": "^16.14.0",
"react-bootstrap": "^2.2.2",
"react-dom": "^16.14.0"
"react-dom": "^16.14.0",
"@braintree/sanitize-url": "^7.0.0"
},
"jest": {
"testEnvironment": "jsdom",
Expand Down
18 changes: 16 additions & 2 deletions src/components/badge/Badge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, {useEffect, useMemo} from 'react';
import PropTypes from 'prop-types';
import {omit} from 'ramda';
import {sanitizeUrl} from '@braintree/sanitize-url';
import RBBadge from 'react-bootstrap/Badge';
import Link from '../../private/Link';
import {bootstrapColors} from '../../private/BootstrapColors';
Expand All @@ -22,6 +23,11 @@ const Badge = props => {
...otherProps
} = props;


const sanitizedUrl = useMemo(() => {
return href ? sanitizeUrl(href) : undefined;
}, [href]);

const incrementClicks = () => {
if (setProps) {
setProps({
Expand All @@ -34,10 +40,18 @@ const Badge = props => {

otherProps[href ? 'preOnClick' : 'onClick'] = incrementClicks;

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

return (
<RBBadge
as={href && Link}
href={href}
href={sanitizedUrl}
bg={isBootstrapColor ? color : null}
text={text_color}
className={class_name || className}
Expand Down
50 changes: 40 additions & 10 deletions src/components/breadcrumb/Breadcrumb.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import React from 'react';
import React, {useEffect, useMemo} from 'react';
import PropTypes from 'prop-types';
import {sanitizeUrl} from '@braintree/sanitize-url';
import RBBreadcrumb from 'react-bootstrap/Breadcrumb';

import Link from '../../private/Link';

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

const BreadcrumbItem = ({ item, idx, item_class_name, itemClassName, setProps }) => {

const sanitizedUrl = useMemo(() => {
return item.href ? sanitizeUrl(item.href) : undefined;
}, [item.href]);

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

return (
<RBBreadcrumb.Item
key={`${item.value}${idx}`}
active={item.active}
linkAs={sanitizedUrl && Link}
className={item_class_name || itemClassName}
href={sanitizedUrl}
linkProps={sanitizedUrl && {external_link: item.external_link}}
>
{item.label}
</RBBreadcrumb.Item>
);
};


const Breadcrumb = ({
items,
tag,
Expand All @@ -16,6 +47,7 @@ const Breadcrumb = ({
item_class_name,
itemClassName,
item_style,
setProps,
...otherProps
}) => (
<RBBreadcrumb
Expand All @@ -27,16 +59,14 @@ 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>
idx={idx}
item={item}
item_class_name={item_class_name}
itemClassName={itemClassName}
setProps={setProps}
/>
))}
</RBBreadcrumb>
);
Expand Down
22 changes: 19 additions & 3 deletions src/components/button/Button.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, {useEffect, useMemo} from 'react';
import PropTypes from 'prop-types';
import {omit} from 'ramda';
import {sanitizeUrl} from '@braintree/sanitize-url';
import RBButton from 'react-bootstrap/Button';
import Link from '../../private/Link';

Expand Down Expand Up @@ -34,6 +35,12 @@ const Button = props => {
...otherProps
} = props;


const sanitizedUrl = useMemo(() => {
return href ? sanitizeUrl(href) : undefined;
}, [href]);


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

if (useLink) {
Expand All @@ -51,12 +58,21 @@ const Button = props => {
otherProps['linkTarget'] = target;
}

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


return (
<RBButton
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
19 changes: 18 additions & 1 deletion src/components/card/CardLink.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, {useEffect, useMemo} from 'react';
import PropTypes from 'prop-types';
import {omit} from 'ramda';
import {sanitizeUrl} from '@braintree/sanitize-url';
import RBCard from 'react-bootstrap/Card';
import Link from '../../private/Link';

Expand All @@ -15,9 +16,16 @@ const CardLink = props => {
disabled,
className,
class_name,
href,
setProps,
...otherProps
} = props;


const sanitizedUrl = useMemo(() => {
return href ? sanitizeUrl(href) : undefined;
}, [href]);

const incrementClicks = () => {
if (!disabled && props.setProps) {
props.setProps({
Expand All @@ -27,6 +35,14 @@ const CardLink = props => {
}
};

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

return (
<RBCard.Link
data-dash-is-loading={
Expand All @@ -35,6 +51,7 @@ const CardLink = props => {
as={Link}
preOnClick={incrementClicks}
disabled={disabled}
href={sanitizedUrl}
className={class_name || className}
{...omit(['setProps', 'n_clicks', 'n_clicks_timestamp'], otherProps)}
>
Expand Down
19 changes: 16 additions & 3 deletions src/components/dropdownmenu/DropdownMenuItem.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useContext} from 'react';
import React, {useContext, useEffect, useMemo} from 'react';
import PropTypes from 'prop-types';
import {omit} from 'ramda';
import {sanitizeUrl} from '@braintree/sanitize-url';
import RBDropdown from 'react-bootstrap/Dropdown';

import Link from '../../private/Link';
Expand All @@ -26,6 +27,18 @@ const DropdownMenuItem = props => {
...otherProps
} = props;

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]);

const context = useContext(DropdownMenuContext);

const handleClick = e => {
Expand All @@ -40,7 +53,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 +65,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
19 changes: 16 additions & 3 deletions src/components/listgroup/ListGroupItem.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, {useEffect, useMemo} from 'react';
import PropTypes from 'prop-types';
import {omit} from 'ramda';
import {sanitizeUrl} from '@braintree/sanitize-url';
import RBListGroupItem from 'react-bootstrap/ListGroupItem';
import Link from '../../private/Link';
import {bootstrapColors} from '../../private/BootstrapColors';
Expand All @@ -24,6 +25,18 @@ const ListGroupItem = props => {
...otherProps
} = props;

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]);

const incrementClicks = () => {
if (!disabled && setProps) {
setProps({
Expand All @@ -33,13 +46,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
22 changes: 18 additions & 4 deletions src/components/nav/NavLink.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, {useEffect, useState} from 'react';
import React, {useEffect, useState, useMemo} from 'react';
import PropTypes from 'prop-types';
import {omit} from 'ramda';
import classNames from 'classnames';
import {History} from '@plotly/dash-component-plugins';
import Link from '../../private/Link';
import {sanitizeUrl} from '@braintree/sanitize-url';

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

const sanitizedUrl = useMemo(() => {
return href ? sanitizeUrl(href) : undefined;
}, [href]);


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 @@ -56,12 +62,20 @@ const NavLink = props => {
disabled
});

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

return (
<Link
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
Loading