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
This commit is contained in:
@@ -25,22 +25,25 @@ export class InfoBasePath extends React.Component {
|
||||
|
||||
class Contact extends React.Component {
|
||||
static propTypes = {
|
||||
data: PropTypes.object
|
||||
data: PropTypes.object,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render(){
|
||||
let { data } = this.props
|
||||
let { data, getComponent } = this.props
|
||||
let name = data.get("name") || "the developer"
|
||||
let url = data.get("url")
|
||||
let email = data.get("email")
|
||||
|
||||
const Link = getComponent("Link")
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ url && <div><a href={ sanitizeUrl(url) } target="_blank">{ name } - Website</a></div> }
|
||||
{ url && <div><Link href={ sanitizeUrl(url) } target="_blank">{ name } - Website</Link></div> }
|
||||
{ email &&
|
||||
<a href={sanitizeUrl(`mailto:${email}`)}>
|
||||
<Link href={sanitizeUrl(`mailto:${email}`)}>
|
||||
{ url ? `Send email to ${name}` : `Contact ${name}`}
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
@@ -49,18 +52,23 @@ class Contact extends React.Component {
|
||||
|
||||
class License extends React.Component {
|
||||
static propTypes = {
|
||||
license: PropTypes.object
|
||||
license: PropTypes.object,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
|
||||
}
|
||||
|
||||
render(){
|
||||
let { license } = this.props
|
||||
let { license, getComponent } = this.props
|
||||
|
||||
const Link = getComponent("Link")
|
||||
|
||||
let name = license.get("name") || "License"
|
||||
let url = license.get("url")
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
url ? <a target="_blank" href={ sanitizeUrl(url) }>{ name }</a>
|
||||
url ? <Link target="_blank" href={ sanitizeUrl(url) }>{ name }</Link>
|
||||
: <span>{ name }</span>
|
||||
}
|
||||
</div>
|
||||
@@ -70,12 +78,17 @@ class License extends React.Component {
|
||||
|
||||
export class InfoUrl extends React.PureComponent {
|
||||
static propTypes = {
|
||||
url: PropTypes.string.isRequired
|
||||
url: PropTypes.string.isRequired,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { url } = this.props
|
||||
return <a target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url } </span></a>
|
||||
const { url, getComponent } = this.props
|
||||
|
||||
const Link = getComponent("Link")
|
||||
|
||||
return <Link target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url } </span></Link>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +113,7 @@ export default class Info extends React.Component {
|
||||
const { url:externalDocsUrl, description:externalDocsDescription } = (externalDocs || fromJS({})).toJS()
|
||||
|
||||
const Markdown = getComponent("Markdown")
|
||||
const Link = getComponent("Link")
|
||||
const VersionStamp = getComponent("VersionStamp")
|
||||
const InfoUrl = getComponent("InfoUrl")
|
||||
const InfoBasePath = getComponent("InfoBasePath")
|
||||
@@ -111,7 +125,7 @@ export default class Info extends React.Component {
|
||||
{ version && <VersionStamp version={version}></VersionStamp> }
|
||||
</h2>
|
||||
{ host || basePath ? <InfoBasePath host={ host } basePath={ basePath } /> : null }
|
||||
{ url && <InfoUrl url={url} /> }
|
||||
{ url && <InfoUrl getComponent={getComponent} url={url} /> }
|
||||
</hgroup>
|
||||
|
||||
<div className="description">
|
||||
@@ -120,14 +134,14 @@ export default class Info extends React.Component {
|
||||
|
||||
{
|
||||
termsOfService && <div>
|
||||
<a target="_blank" href={ sanitizeUrl(termsOfService) }>Terms of service</a>
|
||||
<Link target="_blank" href={ sanitizeUrl(termsOfService) }>Terms of service</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
{ contact && contact.size ? <Contact data={ contact } /> : null }
|
||||
{ license && license.size ? <License license={ license } /> : null }
|
||||
{contact && contact.size ? <Contact getComponent={getComponent} data={ contact } /> : null }
|
||||
{license && license.size ? <License getComponent={getComponent} license={ license } /> : null }
|
||||
{ externalDocsUrl ?
|
||||
<a target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</a>
|
||||
<Link target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
|
||||
: null }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -196,7 +196,7 @@ export class Select extends React.Component {
|
||||
export class Link extends React.Component {
|
||||
|
||||
render() {
|
||||
return <a {...this.props} className={xclass(this.props.className, "link")}/>
|
||||
return <a {...this.props} rel="noopener noreferrer" className={xclass(this.props.className, "link")}/>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default class OnlineValidatorBadge extends React.Component {
|
||||
}
|
||||
|
||||
return (<span style={{ float: "right"}}>
|
||||
<a target="_blank" href={`${ sanitizedValidatorUrl }/debug?url=${ encodeURIComponent(this.state.url) }`}>
|
||||
<a target="_blank" rel="noopener noreferrer" href={`${ sanitizedValidatorUrl }/debug?url=${ encodeURIComponent(this.state.url) }`}>
|
||||
<ValidatorImage src={`${ sanitizedValidatorUrl }?url=${ encodeURIComponent(this.state.url) }`} alt="Online validator badge"/>
|
||||
</a>
|
||||
</span>)
|
||||
|
||||
@@ -46,6 +46,7 @@ export default class OperationTag extends React.Component {
|
||||
const Collapse = getComponent("Collapse")
|
||||
const Markdown = getComponent("Markdown")
|
||||
const DeepLink = getComponent("DeepLink")
|
||||
const Link = getComponent("Link")
|
||||
|
||||
let tagDescription = tagObj.getIn(["tagDetails", "description"], null)
|
||||
let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"])
|
||||
@@ -78,11 +79,11 @@ export default class OperationTag extends React.Component {
|
||||
{ tagExternalDocsDescription }
|
||||
{ tagExternalDocsUrl ? ": " : null }
|
||||
{ tagExternalDocsUrl ?
|
||||
<a
|
||||
<Link
|
||||
href={sanitizeUrl(tagExternalDocsUrl)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
target={"_blank"}
|
||||
>{tagExternalDocsUrl}</a> : null
|
||||
target="_blank"
|
||||
>{tagExternalDocsUrl}</Link> : null
|
||||
}
|
||||
</small>
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ export default class Operation extends PureComponent {
|
||||
const OperationServers = getComponent( "OperationServers" )
|
||||
const OperationExt = getComponent( "OperationExt" )
|
||||
const OperationSummary = getComponent( "OperationSummary" )
|
||||
const Link = getComponent( "Link" )
|
||||
|
||||
const { showExtensions } = getConfigs()
|
||||
|
||||
@@ -134,7 +135,7 @@ export default class Operation extends PureComponent {
|
||||
<span className="opblock-external-docs__description">
|
||||
<Markdown source={ externalDocs.description } />
|
||||
</span>
|
||||
<a target="_blank" className="opblock-external-docs__link" href={ sanitizeUrl(externalDocs.url) }>{ externalDocs.url }</a>
|
||||
<Link target="_blank" className="opblock-external-docs__link" href={sanitizeUrl(externalDocs.url)}>{externalDocs.url}</Link>
|
||||
</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
@@ -4,6 +4,17 @@ import Remarkable from "remarkable"
|
||||
import DomPurify from "dompurify"
|
||||
import cx from "classnames"
|
||||
|
||||
DomPurify.addHook("beforeSanitizeElements", function (current, ) {
|
||||
// Attach safe `rel` values to all elements that contain an `href`,
|
||||
// i.e. all anchors that are links.
|
||||
// We _could_ just look for elements that have a non-self target,
|
||||
// but applying it more broadly shouldn't hurt anything, and is safer.
|
||||
if (current.href) {
|
||||
current.setAttribute("rel", "noopener noreferrer")
|
||||
}
|
||||
return current
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const isPlainText = (str) => /^[A-Z\s0-9!?\.]+$/gi.test(str)
|
||||
|
||||
@@ -15,13 +26,16 @@ function Markdown({ source, className = "" }) {
|
||||
{source}
|
||||
</div>
|
||||
}
|
||||
const html = new Remarkable({
|
||||
|
||||
const md = new Remarkable({
|
||||
html: true,
|
||||
typographer: true,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
linkTarget: "_blank"
|
||||
}).render(source)
|
||||
})
|
||||
|
||||
const html = md.render(source)
|
||||
const sanitized = sanitizer(html)
|
||||
|
||||
if ( !source || !html || !sanitized ) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { sanitizer } from "core/components/providers/markdown"
|
||||
|
||||
const parser = new Remarkable("commonmark")
|
||||
|
||||
parser.set({ linkTarget: "_blank" })
|
||||
|
||||
export const Markdown = ({ source, className = "" }) => {
|
||||
if ( source ) {
|
||||
const html = parser.render(source)
|
||||
|
||||
Reference in New Issue
Block a user