Skip to content
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

Extend safeHref #58

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ setClassNamePrefix('📦')

### Safe `href`s

By default `ui-box` does not ensure that urls use safe protocols when passed to an element. But we built this functionality into `ui-box` to protect the end users of the products you are building. You can alter this by using `configureSafeHref({enabled?: boolean, origin?: string})`. This will ensure that only safe protocols are used (`http:`, `https:`, `mailto:`, `tel:`, and `data:`) and that the correct `rel` values are added (`noopener`, `noreferrer`(for external links)).
By default `ui-box` does not ensure that urls use safe protocols when passed to an element. But we built this functionality into `ui-box` to protect the end users of the products you are building. You can alter this by using `configureSafeHref({enabled?: boolean, origin?: string, additionalProtocols?: string[]})`. This will ensure that only safe protocols are used (`http:`, `https:`, `mailto:`, and `tel:`), that the correct `rel` values are added (`noopener`, `noreferrer`(for external links)), and any additional protocols passed are treated as safe.

```js
import { configureSafeHref } from 'ui-box'
Expand All @@ -320,14 +320,16 @@ configureSafeHref({
import { configureSafeHref } from 'ui-box'
configureSafeHref({
enabled: true
origin: 'https://app.segmentio.com',
origin: 'https://app.segmentio.com',
additionalProtocols: ['data:'],
})
```

Additionally you can overwrite the behavoir on an individual component basis using the prop `allowUnsafeHref`
Additionally you can overwrite the behavoir on an individual component basis using the prop `allowUnsafeHref` and `allowProtocol`. Setting `allowUnsafeHref` completely bypasses all safeHref functionality (protocol checks, rel checks) whereas `allowProtocol` only bypasses protocol checks.

```jsx
<Box is="a" href="javascript:alert('hi')" allowUnsafeHref={true}>This is unsafe</Box>
<Box is="a" href="data:text/html,<html><h1>Hi</h1><script>alert('hi')</script></html>" allowProtocol={true}>This is unsafe</Box>
```

### Server side rendering
Expand Down
15 changes: 12 additions & 3 deletions src/box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import PropTypes from 'prop-types'
import {BoxComponent} from './types/box-types'
import {propTypes} from './enhancers'
import enhanceProps from './enhance-props'
import {extractAnchorProps, getUseSafeHref} from './utils/safeHref'
import {extractAnchorProps, getUseSafeHref, HrefData} from './utils/safeHref'

const Box: BoxComponent = ({ is = 'div', innerRef, children, allowUnsafeHref, ...props }) => {
const Box: BoxComponent = ({ is = 'div', innerRef, children, allowUnsafeHref, allowProtocol, ...props }) => {
// Convert the CSS props to class names (and inject the styles)
const {className, enhancedProps: parsedProps} = enhanceProps(props)

Expand All @@ -22,7 +22,16 @@ const Box: BoxComponent = ({ is = 'div', innerRef, children, allowUnsafeHref, ..
*/
const safeHrefEnabled = (typeof allowUnsafeHref === 'boolean' ? !allowUnsafeHref : getUseSafeHref()) && is === 'a' && parsedProps.href
if (safeHrefEnabled) {
const {safeHref, safeRel} = extractAnchorProps(parsedProps.href, parsedProps.rel)
const hrefData:HrefData = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: What's the motivation for explicitly marking the type here? What happens if you remove :HrefData?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we opt to keep this as-is, I would suggest a space between hrefData: and HrefData.

href: parsedProps.href,
rel: parsedProps.rel,
}

if (allowProtocol) {
hrefData.allowProtocol = allowProtocol
colinlohner marked this conversation as resolved.
Show resolved Hide resolved
}

const {safeHref, safeRel} = extractAnchorProps(hrefData)
parsedProps.href = safeHref
parsedProps.rel = safeRel
}
Expand Down
5 changes: 5 additions & 0 deletions src/types/box-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export type BoxProps<T extends Is> = InheritedProps<T> &
* Allows the high level value of safeHref to be overwritten on an individual component basis
*/
allowUnsafeHref?: boolean

/**
* Allows an unsafe protocol to be used on an individual component basis
*/
allowProtocol?: boolean
}

export interface BoxComponent {
Expand Down
30 changes: 20 additions & 10 deletions src/utils/safeHref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ export interface URLInfo {
sameOrigin: boolean
}

export interface HrefData {
href: string
rel: string
allowProtocol?: boolean
}

export interface SafeHrefConfigObj {
enabled?: boolean
origin?: string
additionalProtocols?: string[]
}

const PROTOCOL_REGEX = /^[a-z]+:/
const ORIGIN_REGEX = /^(?:[a-z]+:?:)?(?:\/\/)?([^\/\?]+)/
const safeProtocols = ['http:', 'https:', 'mailto:', 'tel:']
let useSafeHref = false
let globalOrigin = typeof window !== 'undefined' ? window.location.origin : false

Expand All @@ -21,33 +29,32 @@ export function configureSafeHref(configObject: SafeHrefConfigObj) {
if (configObject.origin) {
globalOrigin = configObject.origin
}

if (configObject.additionalProtocols && configObject.additionalProtocols.length) {
safeProtocols.push(...configObject.additionalProtocols)
colinlohner marked this conversation as resolved.
Show resolved Hide resolved
}
}

export function getUseSafeHref(): boolean {
return useSafeHref
}

export function getURLInfo(url: string): URLInfo {
/**
* An array of the safely allowed url protocols
*/
const safeProtocols = ['http:', 'https:', 'mailto:', 'tel:', 'data:']

export function getURLInfo(url: string, allowProtocol: boolean): URLInfo {
/**
* - Find protocol of URL or set to 'relative'
* - Find origin of URL
* - Determine if sameOrigin
* - Determine if protocol of URL is safe
*/
const protocolResult = url.match(PROTOCOL_REGEX)
const protocolResult = url.replace(' ', '').match(PROTOCOL_REGEX)
colinlohner marked this conversation as resolved.
Show resolved Hide resolved
const originResult = url.match(ORIGIN_REGEX)
const urlProtocol = protocolResult ? protocolResult[0] : 'relative'
let sameOrigin = urlProtocol === 'relative'
if (!sameOrigin && globalOrigin) {
sameOrigin = globalOrigin === (originResult && originResult[0])
}

const isSafeProtocol = sameOrigin ? true : safeProtocols.includes(urlProtocol)
const isSafeProtocol = (allowProtocol || sameOrigin) ? true : safeProtocols.includes(urlProtocol)
if (!isSafeProtocol) {
/**
* If the url is unsafe, put a error in the console, and return the URLInfo object
Expand All @@ -72,11 +79,14 @@ export function getURLInfo(url: string): URLInfo {
}
}

export function extractAnchorProps(href: string, rel: string) {
export function extractAnchorProps(hrefData: HrefData) {
const {href, rel} = hrefData
const allowProtocol = hrefData.allowProtocol || false

/**
* Get url info and update href
*/
const urlInfo = getURLInfo(href)
const urlInfo = getURLInfo(href, allowProtocol)
const safeHref = urlInfo.url

/**
Expand Down
4 changes: 3 additions & 1 deletion tools/story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ storiesOf('Box', module)
})
.add('safe `href`', () => {
configureSafeHref({
enabled: true
enabled: true,
})
return (
<Box paddingTop={30} borderTop="1px solid" marginTop={30}>
Expand All @@ -45,6 +45,8 @@ storiesOf('Box', module)
<Box is="a" href="http://localhost:9009/test">Same Origin Link</Box>
<Box is="a" href="https://apple.com">External Link</Box>
<Box is="a" href="javascript:alert('hi')">Javascript protocol Link</Box>
<Box is="a" href=" data:text/html,<html><h1>Hi</h1></html>">Data protocol Link</Box>
<Box is="a" href=" data:text/html,<html><h1>Hi</h1><script>alert('hi')</script></html>" allowProtocol={true}>Allow protocol Link</Box>
<Box is="a" href="javascript:alert('hi')" allowUnsafeHref={true}>Overwride Safe Href</Box>
</Box>
)
Expand Down