diff --git a/package.json b/package.json index fb196b52d..da526a251 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/badge/Badge.js b/src/components/badge/Badge.js index ee0ac9366..5d43ac1e2 100644 --- a/src/components/badge/Badge.js +++ b/src/components/badge/Badge.js @@ -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'; @@ -22,6 +23,11 @@ const Badge = props => { ...otherProps } = props; + +const sanitizedUrl = useMemo(() => { + return href ? sanitizeUrl(href) : undefined; + }, [href]); + const incrementClicks = () => { if (setProps) { setProps({ @@ -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 ( { + + 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 ( + + {item.label} + + ); +}; + + const Breadcrumb = ({ items, tag, @@ -16,6 +47,7 @@ const Breadcrumb = ({ item_class_name, itemClassName, item_style, + setProps, ...otherProps }) => ( {(items || []).map((item, idx) => ( - - {item.label} - + idx={idx} + item={item} + item_class_name={item_class_name} + itemClassName={itemClassName} + setProps={setProps} + /> ))} ); diff --git a/src/components/button/Button.js b/src/components/button/Button.js index 4a1d26792..f92f1e4da 100644 --- a/src/components/button/Button.js +++ b/src/components/button/Button.js @@ -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'; @@ -34,6 +35,12 @@ const Button = props => { ...otherProps } = props; + + const sanitizedUrl = useMemo(() => { + return href ? sanitizeUrl(href) : undefined; + }, [href]); + + const incrementClicks = () => { if (!disabled && setProps) { setProps({ @@ -42,7 +49,7 @@ const Button = props => { }); } }; - const useLink = href && !disabled; + const useLink = sanitizedUrl && !disabled; otherProps[useLink ? 'preOnClick' : 'onClick'] = onClick || incrementClicks; if (useLink) { @@ -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 ( { 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({ @@ -27,6 +35,14 @@ const CardLink = props => { } }; + useEffect(() => { + if (sanitizedUrl && sanitizedUrl !== href) { + setProps({ + _dash_error: new Error(`Dangerous link detected:: ${href}`), + }); + } + }, [href, sanitizedUrl]); + return ( { as={Link} preOnClick={incrementClicks} disabled={disabled} + href={sanitizedUrl} className={class_name || className} {...omit(['setProps', 'n_clicks', 'n_clicks_timestamp'], otherProps)} > diff --git a/src/components/dropdownmenu/DropdownMenuItem.js b/src/components/dropdownmenu/DropdownMenuItem.js index 7a19d6ca9..a96b8a5d3 100644 --- a/src/components/dropdownmenu/DropdownMenuItem.js +++ b/src/components/dropdownmenu/DropdownMenuItem.js @@ -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'; @@ -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 => { @@ -40,7 +53,7 @@ const DropdownMenuItem = props => { } }; - const useLink = href && !disabled; + const useLink = sanitizedUrl && !disabled; otherProps[useLink ? 'preOnClick' : 'onClick'] = e => handleClick(e); if (header) { @@ -52,7 +65,7 @@ const DropdownMenuItem = props => { return ( { ...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({ @@ -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 ( { ...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)) ); }; @@ -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 ( { - const {children, loading_state, className, class_name, ...otherProps} = props; + const {children, loading_state, className, class_name, href, setProps, ...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]); + return ( { loading_state, className, class_name, + setProps, ...otherProps } = props; + + const sanitizedUrl = useMemo(() => { + return brand_href ? sanitizeUrl(brand_href) : undefined; + }, [brand_href]); + + const isBootstrapColor = bootstrapColors.has(color); const [navbarOpen, setNavbarOpen] = useState(false); const toggle = () => setNavbarOpen(!navbarOpen); + useEffect(() => { + if (sanitizedUrl && sanitizedUrl !== brand_href) { + setProps({ + _dash_error: new Error(`Dangerous link detected:: ${brand_href}`), + }); + } + }, [brand_href, sanitizedUrl]); + return ( { {brand && ( diff --git a/src/index.js b/src/index.js index b8d40bb18..fe21f3fcd 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,7 @@ export {default as CardHeader} from './components/card/CardHeader'; export {default as CardImg} from './components/card/CardImg'; export {default as CardImgOverlay} from './components/card/CardImgOverlay'; export {default as CardLink} from './components/card/CardLink'; -export {default as Carousel} from './components/carousel/Carousel'; +export {default as Carousel} from './components/carousel/Carousel';`` export {default as Checkbox} from './components/input/Checkbox'; export {default as Checklist} from './components/input/Checklist'; export {default as Col} from './components/layout/Col'; diff --git a/usage.py b/usage.py new file mode 100644 index 000000000..b4fd0165b --- /dev/null +++ b/usage.py @@ -0,0 +1,70 @@ +from dash import Dash, html +import dash_bootstrap_components as dbc + +app = Dash(__name__) + +NavLink1 = dbc.NavLink("dbc link1") +NavLink2 = dbc.NavLink("dbc link2", href="javascript:alert('NavLink')") + +NavbarSimple = dbc.NavbarSimple( + brand="NavbarSimple", + brand_href="javascript:alert('NavbarSimple')", +) +Badge = dbc.Badge("badge", href="javascript:alert('Badge')") + + +Breadcrumb = dbc.Breadcrumb( + items=[ + { + "label": "Docs", + "href": "javascript:alert('Breadcrumb1')", + "external_link": True, + }, + { + "label": "Components", + "href": "javascript:alert('Breadcrumb2')", + "external_link": True, + }, + {"label": "Breadcrumb", "active": True}, + ], +) + +Button = dbc.Button("button", href="javascript:alert('Button')") + +CardLink = dbc.Card(dbc.CardLink("cardlink", href="javascript:alert('Card')")) + +ListGroupItem = dbc.ListGroup( + [ + dbc.ListGroupItem("Active item", active=True), + dbc.ListGroupItem("Item 2", href="javascript:alert('ListGroupItem')"), + ] +) + +NavbarBrand = dbc.Navbar( + dbc.NavbarBrand("Navbar Brand", href="javascript:alert('NavbarBrand')") +) + +DropdownMenuItem = dbc.DropdownMenu( + dbc.DropdownMenuItem( + "DropdownMenuItem", href="javascript:alert('DropdownMenuItem')" + ) +) + + +app.layout = html.Div( + [ + NavLink1, + NavLink2, + NavbarSimple, + Badge, + Breadcrumb, + Button, + CardLink, + ListGroupItem, + NavbarBrand, + DropdownMenuItem, + ] +) + +if __name__ == "__main__": + app.run_server(debug=True)