diff --git a/README.md b/README.md
index eb8dc8a..0165b5d 100644
--- a/README.md
+++ b/README.md
@@ -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'
@@ -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 `allowProtocols`. Setting `allowUnsafeHref` completely bypasses all safeHref functionality (protocol checks, rel checks). Setting `allowProtocols` adds the contents of a string array to the allowed protocols.
```jsx
This is unsafe
+This is unsafe
```
### Server side rendering
diff --git a/src/box.tsx b/src/box.tsx
index 3e4d331..ffe77ad 100644
--- a/src/box.tsx
+++ b/src/box.tsx
@@ -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, allowProtocols, ...props }) => {
// Convert the CSS props to class names (and inject the styles)
const {className, enhancedProps: parsedProps} = enhanceProps(props)
@@ -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 = {
+ href: parsedProps.href,
+ rel: parsedProps.rel,
+ }
+
+ if (allowProtocols && allowProtocols.length > 0) {
+ hrefData.allowProtocols = allowProtocols
+ }
+
+ const {safeHref, safeRel} = extractAnchorProps(hrefData)
parsedProps.href = safeHref
parsedProps.rel = safeRel
}
diff --git a/src/types/box-types.ts b/src/types/box-types.ts
index ed7cf67..71ec8aa 100644
--- a/src/types/box-types.ts
+++ b/src/types/box-types.ts
@@ -59,6 +59,11 @@ export type BoxProps = InheritedProps &
* Allows the high level value of safeHref to be overwritten on an individual component basis
*/
allowUnsafeHref?: boolean
+
+ /**
+ * Allows additional protocols to be considered safe
+ */
+ allowProtocols?: Array
}
export interface BoxComponent {
diff --git a/src/utils/safeHref.ts b/src/utils/safeHref.ts
index 74af1fd..09a8766 100644
--- a/src/utils/safeHref.ts
+++ b/src/utils/safeHref.ts
@@ -3,13 +3,22 @@ export interface URLInfo {
sameOrigin: boolean
}
+export interface HrefData {
+ href: string
+ rel: string
+ allowProtocols?: string[]
+}
+
export interface SafeHrefConfigObj {
enabled?: boolean
origin?: string
+ additionalProtocols?: string[]
}
const PROTOCOL_REGEX = /^[a-z]+:/
const ORIGIN_REGEX = /^(?:[a-z]+:?:)?(?:\/\/)?([^\/\?]+)/
+const safeProtocols: string[] = ['http:', 'https:', 'mailto:', 'tel:']
+let customProtocols:Array = []
let useSafeHref = false
let globalOrigin = typeof window !== 'undefined' ? window.location.origin : false
@@ -21,40 +30,45 @@ export function configureSafeHref(configObject: SafeHrefConfigObj) {
if (configObject.origin) {
globalOrigin = configObject.origin
}
+
+ if (configObject.additionalProtocols && configObject.additionalProtocols.length) {
+ customProtocols.push(...configObject.additionalProtocols)
+ }
}
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 resetCustomProtocols() {
+ customProtocols = []
+}
+export function getURLInfo(url: string, allowProtocols: Array): 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 originResult = url.match(ORIGIN_REGEX)
+ const cleanedUrl = url.trim()
+ const protocolResult = cleanedUrl.match(PROTOCOL_REGEX)
+ const originResult = cleanedUrl.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 allowedProtocols = [...safeProtocols, ...customProtocols, ...allowProtocols]
+ const isSafeProtocol = sameOrigin ? true : allowedProtocols.includes(urlProtocol)
if (!isSafeProtocol) {
/**
* If the url is unsafe, put a error in the console, and return the URLInfo object
* with the value of url being `undefined`
*/
console.error(
- '📦 `href` passed to anchor tag is unsafe. Because of this, the `href` on the element was not set. Please review the safe href documentation if you have questions.',
+ '📦 ui-box: `href` passed to anchor tag is unsafe. Because of this, the `href` on the element was not set. Please review the safe href documentation if you have questions.',
'https://www.github.com/segmentio/ui-box'
)
return {
@@ -67,16 +81,19 @@ export function getURLInfo(url: string): URLInfo {
* If the url is safe, return the url and origin
*/
return {
- url,
+ url: cleanedUrl,
sameOrigin
}
}
-export function extractAnchorProps(href: string, rel: string) {
+export function extractAnchorProps(hrefData: HrefData) {
+ const {href, rel} = hrefData
+ const allowProtocols = hrefData.allowProtocols && hrefData.allowProtocols.length ? hrefData.allowProtocols : []
+
/**
* Get url info and update href
*/
- const urlInfo = getURLInfo(href)
+ const urlInfo = getURLInfo(href, allowProtocols)
const safeHref = urlInfo.url
/**
diff --git a/test/utils/safeHref.ts b/test/utils/safeHref.ts
new file mode 100644
index 0000000..7d2ec19
--- /dev/null
+++ b/test/utils/safeHref.ts
@@ -0,0 +1,58 @@
+import test from 'ava'
+import {extractAnchorProps, configureSafeHref, resetCustomProtocols} from '../../src/utils/safeHref'
+
+test('Allows safe protocols', t => {
+ configureSafeHref({
+ enabled: true
+ })
+
+ const {safeHref} = extractAnchorProps({
+ href: 'https://www.apple.com',
+ rel: ''
+ })
+
+ t.assert(safeHref === 'https://www.apple.com')
+})
+
+test('Rejects unsafe protocols', t => {
+ const {safeHref} = extractAnchorProps({
+ href: 'javascript:alert("hi")',
+ rel: ''
+ })
+
+ t.assert(safeHref === undefined)
+})
+
+test('Rejects unsafe protocols with whitespace', t => {
+ const {safeHref} = extractAnchorProps({
+ href: ' javascript:alert("hi")',
+ rel: ''
+ })
+
+ t.assert(safeHref === undefined)
+})
+
+test('Allows custom protocol', t => {
+ configureSafeHref({
+ additionalProtocols: ['data:']
+ })
+
+ const {safeHref} = extractAnchorProps({
+ href: 'data:text/html,Hi
',
+ rel: ''
+ })
+
+ resetCustomProtocols()
+
+ t.assert(safeHref === 'data:text/html,Hi
')
+})
+
+test('Allows individual level custom protocol', t => {
+ const {safeHref} = extractAnchorProps({
+ href: 'data:text/html,Hi
',
+ rel: '',
+ allowProtocols: ['data:']
+ })
+
+ t.assert(safeHref === 'data:text/html,Hi
')
+})
diff --git a/tools/story.tsx b/tools/story.tsx
index 06abee0..7fd30b2 100644
--- a/tools/story.tsx
+++ b/tools/story.tsx
@@ -36,7 +36,7 @@ storiesOf('Box', module)
})
.add('safe `href`', () => {
configureSafeHref({
- enabled: true
+ enabled: true,
})
return (
@@ -45,6 +45,8 @@ storiesOf('Box', module)
Same Origin Link
External Link
Javascript protocol Link
+ Data protocol Link
+ Allow protocol Link
Overwride Safe Href
)