Skip to content

Commit dd3afdc

Browse files
authored
fix: anchor tag safety (via #4789)
* v3.17.6 * release(3.17.6): rebuild dist * add failing tests * fix Link component * fix OnlineValidatorBadge component * switch from <a> to <Link> in operation components * make Markdown inputs safe * use Link component in Info block, for target safety * add eslint rule for unsafe `target` usage
1 parent fe5b234 commit dd3afdc

File tree

15 files changed

+354
-31
lines changed

15 files changed

+354
-31
lines changed

.eslintrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
"import"
2525
],
2626

27+
"settings": {
28+
"react": {
29+
"pragma": "React",
30+
"version": "15.0"
31+
}
32+
},
33+
2734
"rules": {
2835
"semi": [2, "never"],
2936
"strict": 0,
@@ -37,6 +44,7 @@
3744
"comma-dangle": 0,
3845
"no-console": ["error", { allow: ["warn", "error"] }],
3946
"react/jsx-no-bind": 1,
47+
"react/jsx-no-target-blank": 2,
4048
"react/display-name": 0,
4149
"mocha/no-exclusive-tests": "error",
4250
"import/no-extraneous-dependencies": [2]

package-lock.json

Lines changed: 38 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
"eslint": "^4.1.1",
101101
"eslint-plugin-import": "^2.13.0",
102102
"eslint-plugin-mocha": "^4.11.0",
103-
"eslint-plugin-react": "~7.7.0",
103+
"eslint-plugin-react": "^7.10.0",
104104
"expect": "^1.20.2",
105105
"extract-text-webpack-plugin": "^3.0.2",
106106
"file-loader": "^1.1.11",

src/core/components/info.jsx

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,25 @@ export class InfoBasePath extends React.Component {
2525

2626
class Contact extends React.Component {
2727
static propTypes = {
28-
data: PropTypes.object
28+
data: PropTypes.object,
29+
getComponent: PropTypes.func.isRequired
2930
}
3031

3132
render(){
32-
let { data } = this.props
33+
let { data, getComponent } = this.props
3334
let name = data.get("name") || "the developer"
3435
let url = data.get("url")
3536
let email = data.get("email")
3637

38+
const Link = getComponent("Link")
39+
3740
return (
3841
<div>
39-
{ url && <div><a href={ sanitizeUrl(url) } target="_blank">{ name } - Website</a></div> }
42+
{ url && <div><Link href={ sanitizeUrl(url) } target="_blank">{ name } - Website</Link></div> }
4043
{ email &&
41-
<a href={sanitizeUrl(`mailto:${email}`)}>
44+
<Link href={sanitizeUrl(`mailto:${email}`)}>
4245
{ url ? `Send email to ${name}` : `Contact ${name}`}
43-
</a>
46+
</Link>
4447
}
4548
</div>
4649
)
@@ -49,18 +52,23 @@ class Contact extends React.Component {
4952

5053
class License extends React.Component {
5154
static propTypes = {
52-
license: PropTypes.object
55+
license: PropTypes.object,
56+
getComponent: PropTypes.func.isRequired
57+
5358
}
5459

5560
render(){
56-
let { license } = this.props
61+
let { license, getComponent } = this.props
62+
63+
const Link = getComponent("Link")
64+
5765
let name = license.get("name") || "License"
5866
let url = license.get("url")
5967

6068
return (
6169
<div>
6270
{
63-
url ? <a target="_blank" href={ sanitizeUrl(url) }>{ name }</a>
71+
url ? <Link target="_blank" href={ sanitizeUrl(url) }>{ name }</Link>
6472
: <span>{ name }</span>
6573
}
6674
</div>
@@ -70,12 +78,17 @@ class License extends React.Component {
7078

7179
export class InfoUrl extends React.PureComponent {
7280
static propTypes = {
73-
url: PropTypes.string.isRequired
81+
url: PropTypes.string.isRequired,
82+
getComponent: PropTypes.func.isRequired
7483
}
7584

85+
7686
render() {
77-
const { url } = this.props
78-
return <a target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url } </span></a>
87+
const { url, getComponent } = this.props
88+
89+
const Link = getComponent("Link")
90+
91+
return <Link target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url } </span></Link>
7992
}
8093
}
8194

@@ -100,6 +113,7 @@ export default class Info extends React.Component {
100113
const { url:externalDocsUrl, description:externalDocsDescription } = (externalDocs || fromJS({})).toJS()
101114

102115
const Markdown = getComponent("Markdown")
116+
const Link = getComponent("Link")
103117
const VersionStamp = getComponent("VersionStamp")
104118
const InfoUrl = getComponent("InfoUrl")
105119
const InfoBasePath = getComponent("InfoBasePath")
@@ -111,7 +125,7 @@ export default class Info extends React.Component {
111125
{ version && <VersionStamp version={version}></VersionStamp> }
112126
</h2>
113127
{ host || basePath ? <InfoBasePath host={ host } basePath={ basePath } /> : null }
114-
{ url && <InfoUrl url={url} /> }
128+
{ url && <InfoUrl getComponent={getComponent} url={url} /> }
115129
</hgroup>
116130

117131
<div className="description">
@@ -120,14 +134,14 @@ export default class Info extends React.Component {
120134

121135
{
122136
termsOfService && <div>
123-
<a target="_blank" href={ sanitizeUrl(termsOfService) }>Terms of service</a>
137+
<Link target="_blank" href={ sanitizeUrl(termsOfService) }>Terms of service</Link>
124138
</div>
125139
}
126140

127-
{ contact && contact.size ? <Contact data={ contact } /> : null }
128-
{ license && license.size ? <License license={ license } /> : null }
141+
{contact && contact.size ? <Contact getComponent={getComponent} data={ contact } /> : null }
142+
{license && license.size ? <License getComponent={getComponent} license={ license } /> : null }
129143
{ externalDocsUrl ?
130-
<a target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</a>
144+
<Link target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
131145
: null }
132146

133147
</div>

src/core/components/layout-utils.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export class Select extends React.Component {
196196
export class Link extends React.Component {
197197

198198
render() {
199-
return <a {...this.props} className={xclass(this.props.className, "link")}/>
199+
return <a {...this.props} rel="noopener noreferrer" className={xclass(this.props.className, "link")}/>
200200
}
201201

202202
}

src/core/components/online-validator-badge.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class OnlineValidatorBadge extends React.Component {
5454
}
5555

5656
return (<span style={{ float: "right"}}>
57-
<a target="_blank" href={`${ sanitizedValidatorUrl }/debug?url=${ encodeURIComponent(this.state.url) }`}>
57+
<a target="_blank" rel="noopener noreferrer" href={`${ sanitizedValidatorUrl }/debug?url=${ encodeURIComponent(this.state.url) }`}>
5858
<ValidatorImage src={`${ sanitizedValidatorUrl }?url=${ encodeURIComponent(this.state.url) }`} alt="Online validator badge"/>
5959
</a>
6060
</span>)

src/core/components/operation-tag.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default class OperationTag extends React.Component {
4646
const Collapse = getComponent("Collapse")
4747
const Markdown = getComponent("Markdown")
4848
const DeepLink = getComponent("DeepLink")
49+
const Link = getComponent("Link")
4950

5051
let tagDescription = tagObj.getIn(["tagDetails", "description"], null)
5152
let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"])
@@ -78,11 +79,11 @@ export default class OperationTag extends React.Component {
7879
{ tagExternalDocsDescription }
7980
{ tagExternalDocsUrl ? ": " : null }
8081
{ tagExternalDocsUrl ?
81-
<a
82+
<Link
8283
href={sanitizeUrl(tagExternalDocsUrl)}
8384
onClick={(e) => e.stopPropagation()}
84-
target={"_blank"}
85-
>{tagExternalDocsUrl}</a> : null
85+
target="_blank"
86+
>{tagExternalDocsUrl}</Link> : null
8687
}
8788
</small>
8889
}

src/core/components/operation.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export default class Operation extends PureComponent {
9999
const OperationServers = getComponent( "OperationServers" )
100100
const OperationExt = getComponent( "OperationExt" )
101101
const OperationSummary = getComponent( "OperationSummary" )
102+
const Link = getComponent( "Link" )
102103

103104
const { showExtensions } = getConfigs()
104105

@@ -134,7 +135,7 @@ export default class Operation extends PureComponent {
134135
<span className="opblock-external-docs__description">
135136
<Markdown source={ externalDocs.description } />
136137
</span>
137-
<a target="_blank" className="opblock-external-docs__link" href={ sanitizeUrl(externalDocs.url) }>{ externalDocs.url }</a>
138+
<Link target="_blank" className="opblock-external-docs__link" href={sanitizeUrl(externalDocs.url)}>{externalDocs.url}</Link>
138139
</div>
139140
</div> : null
140141
}

src/core/components/providers/markdown.jsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import Remarkable from "remarkable"
44
import DomPurify from "dompurify"
55
import cx from "classnames"
66

7+
DomPurify.addHook("beforeSanitizeElements", function (current, ) {
8+
// Attach safe `rel` values to all elements that contain an `href`,
9+
// i.e. all anchors that are links.
10+
// We _could_ just look for elements that have a non-self target,
11+
// but applying it more broadly shouldn't hurt anything, and is safer.
12+
if (current.href) {
13+
current.setAttribute("rel", "noopener noreferrer")
14+
}
15+
return current
16+
})
17+
718
// eslint-disable-next-line no-useless-escape
819
const isPlainText = (str) => /^[A-Z\s0-9!?\.]+$/gi.test(str)
920

@@ -15,13 +26,16 @@ function Markdown({ source, className = "" }) {
1526
{source}
1627
</div>
1728
}
18-
const html = new Remarkable({
29+
30+
const md = new Remarkable({
1931
html: true,
2032
typographer: true,
2133
breaks: true,
2234
linkify: true,
2335
linkTarget: "_blank"
24-
}).render(source)
36+
})
37+
38+
const html = md.render(source)
2539
const sanitized = sanitizer(html)
2640

2741
if ( !source || !html || !sanitized ) {

src/core/plugins/oas3/wrap-components/markdown.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { sanitizer } from "core/components/providers/markdown"
77

88
const parser = new Remarkable("commonmark")
99

10+
parser.set({ linkTarget: "_blank" })
11+
1012
export const Markdown = ({ source, className = "" }) => {
1113
if ( source ) {
1214
const html = parser.render(source)

0 commit comments

Comments
 (0)