Skip to content

Commit 2b25e65

Browse files
authored
Merge pull request #3848 from swagger-api/bug/3847-href-xss
Add URL sanitizer to avoid `href` XSS attack vector
2 parents 4eae9b6 + 551a82d commit 2b25e65

File tree

7 files changed

+51
-13
lines changed

7 files changed

+51
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"e2e": "npm-run-all --parallel -r hot-server mock-api test-e2e"
4040
},
4141
"dependencies": {
42+
"@braintree/sanitize-url": "^2.0.2",
4243
"base64-js": "^1.2.0",
4344
"brace": "0.7.0",
4445
"classnames": "^2.2.5",

src/core/components/info.jsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react"
22
import PropTypes from "prop-types"
33
import { fromJS } from "immutable"
44
import ImPropTypes from "react-immutable-proptypes"
5+
import { sanitizeUrl } from "core/utils"
56

67

78
class Path extends React.Component {
@@ -35,9 +36,9 @@ class Contact extends React.Component {
3536

3637
return (
3738
<div>
38-
{ url && <div><a href={ url } target="_blank">{ name } - Website</a></div> }
39+
{ url && <div><a href={ sanitizeUrl(url) } target="_blank">{ name } - Website</a></div> }
3940
{ email &&
40-
<a href={`mailto:${email}`}>
41+
<a href={sanitizeUrl(`mailto:${email}`)}>
4142
{ url ? `Send email to ${name}` : `Contact ${name}`}
4243
</a>
4344
}
@@ -59,7 +60,7 @@ class License extends React.Component {
5960
return (
6061
<div>
6162
{
62-
url ? <a target="_blank" href={ url }>{ name }</a>
63+
url ? <a target="_blank" href={ sanitizeUrl(url) }>{ name }</a>
6364
: <span>{ name }</span>
6465
}
6566
</div>
@@ -97,7 +98,7 @@ export default class Info extends React.Component {
9798
{ version && <VersionStamp version={version}></VersionStamp> }
9899
</h2>
99100
{ host || basePath ? <Path host={ host } basePath={ basePath } /> : null }
100-
{ url && <a target="_blank" href={ url }><span className="url"> { url } </span></a> }
101+
{ url && <a target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url } </span></a> }
101102
</hgroup>
102103

103104
<div className="description">
@@ -106,14 +107,14 @@ export default class Info extends React.Component {
106107

107108
{
108109
termsOfService && <div>
109-
<a target="_blank" href={ termsOfService }>Terms of service</a>
110+
<a target="_blank" href={ sanitizeUrl(termsOfService) }>Terms of service</a>
110111
</div>
111112
}
112113

113114
{ contact && contact.size ? <Contact data={ contact } /> : null }
114115
{ license && license.size ? <License license={ license } /> : null }
115116
{ externalDocsUrl ?
116-
<a target="_blank" href={externalDocsUrl}>{externalDocsDescription || externalDocsUrl}</a>
117+
<a target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</a>
117118
: null }
118119

119120
</div>

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react"
22
import PropTypes from "prop-types"
3+
import { sanitizeUrl } from "core/utils"
34

45
export default class OnlineValidatorBadge extends React.Component {
56
static propTypes = {
@@ -32,6 +33,8 @@ export default class OnlineValidatorBadge extends React.Component {
3233
let { getConfigs } = this.props
3334
let { spec } = getConfigs()
3435

36+
let sanitizedValidatorUrl = sanitizeUrl(this.state.validatorUrl)
37+
3538
if ( typeof spec === "object" && Object.keys(spec).length) return null
3639

3740
if (!this.state.url || !this.state.validatorUrl || this.state.url.indexOf("localhost") >= 0
@@ -40,8 +43,8 @@ export default class OnlineValidatorBadge extends React.Component {
4043
}
4144

4245
return (<span style={{ float: "right"}}>
43-
<a target="_blank" href={`${ this.state.validatorUrl }/debug?url=${ this.state.url }`}>
44-
<ValidatorImage src={`${ this.state.validatorUrl }?url=${ this.state.url }`} alt="Online validator badge"/>
46+
<a target="_blank" href={`${ sanitizedValidatorUrl }/debug?url=${ this.state.url }`}>
47+
<ValidatorImage src={`${ sanitizedValidatorUrl }?url=${ this.state.url }`} alt="Online validator badge"/>
4548
</a>
4649
</span>)
4750
}

src/core/components/operation.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { PureComponent } from "react"
22
import PropTypes from "prop-types"
33
import { getList } from "core/utils"
44
import * as CustomPropTypes from "core/proptypes"
5+
import { sanitizeUrl } from "core/utils"
56

67
//import "less/opblock"
78

@@ -206,7 +207,7 @@ export default class Operation extends PureComponent {
206207
<span className="opblock-external-docs__description">
207208
<Markdown source={ externalDocs.get("description") } />
208209
</span>
209-
<a className="opblock-external-docs__link" href={ externalDocs.get("url") }>{ externalDocs.get("url") }</a>
210+
<a className="opblock-external-docs__link" href={ sanitizeUrl(externalDocs.get("url")) }>{ externalDocs.get("url") }</a>
210211
</div>
211212
</div> : null
212213
}

src/core/components/operations.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react"
22
import PropTypes from "prop-types"
33
import { helpers } from "swagger-client"
4-
import { createDeepLinkPath } from "core/utils"
4+
import { createDeepLinkPath, sanitizeUrl } from "core/utils"
55
const { opId } = helpers
66

77
export default class Operations extends React.Component {
@@ -101,7 +101,7 @@ export default class Operations extends React.Component {
101101
{ tagExternalDocsUrl ? ": " : null }
102102
{ tagExternalDocsUrl ?
103103
<a
104-
href={tagExternalDocsUrl}
104+
href={sanitizeUrl(tagExternalDocsUrl)}
105105
onClick={(e) => e.stopPropagation()}
106106
target={"_blank"}
107107
>{tagExternalDocsUrl}</a> : null

src/core/utils.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Im from "immutable"
2-
2+
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"
33
import camelCase from "lodash/camelCase"
44
import upperFirst from "lodash/upperFirst"
55
import _memoize from "lodash/memoize"
@@ -722,6 +722,10 @@ export const shallowEqualKeys = (a,b, keys) => {
722722
})
723723
}
724724

725+
export function sanitizeUrl(url) {
726+
return braintreeSanitizeUrl(url)
727+
}
728+
725729
export function getAcceptControllingResponse(responses) {
726730
if(!Im.OrderedMap.isOrderedMap(responses)) {
727731
// wrong type!

test/core/utils.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
fromJSOrdered,
1717
getAcceptControllingResponse,
1818
createDeepLinkPath,
19-
escapeDeepLinkPath
19+
escapeDeepLinkPath,
20+
sanitizeUrl
2021
} from "core/utils"
2122
import win from "core/window"
2223

@@ -885,4 +886,31 @@ describe("utils", function() {
885886
expect(result).toEqual("hello\\#world")
886887
})
887888
})
889+
890+
describe("sanitizeUrl", function() {
891+
it("should sanitize a `javascript:` url", function() {
892+
const res = sanitizeUrl("javascript:alert('bam!')")
893+
894+
expect(res).toEqual("about:blank")
895+
})
896+
897+
it("should sanitize a `data:` url", function() {
898+
const res = sanitizeUrl(`data:text/html;base64,PHNjcmlwdD5hbGVydCgiSGV
899+
sbG8iKTs8L3NjcmlwdD4=`)
900+
901+
expect(res).toEqual("about:blank")
902+
})
903+
904+
it("should not modify a `http:` url", function() {
905+
const res = sanitizeUrl(`http://swagger.io/`)
906+
907+
expect(res).toEqual("http://swagger.io/")
908+
})
909+
910+
it("should not modify a `https:` url", function() {
911+
const res = sanitizeUrl(`https://swagger.io/`)
912+
913+
expect(res).toEqual("https://swagger.io/")
914+
})
915+
})
888916
})

0 commit comments

Comments
 (0)