export { default as theme } from './theme' import { FullScreenCode, SplitRight } from 'mdx-deck/layouts' import BasicComponent from './components/BasicComponent' import DangerouslySet from './components/DangerouslySet' import DOMPurifyExample from './components/DOMPurifyExample' import URLIssue from './components/URLIssue' import URLIssueFixed from './components/URLIssueFixed'
- Amanvir Sangha
- Security Consultant @ Synopsys SIG
- Interests: Static Analysis, Application Security
- Quick introduction to React
- Securing your React application
- Remediation
- Front-end library for building user interfaces
- Components based
- View rendering only (external dependencies for routing etc.)
- Virtual DOM for faster rendering
- One-way binding components
- Rapid prototyping (e.g.
create-react-app
) - High rate of adoption
- Small attack surface
import React from 'react';
class BasicComponent extends React.Component {
state = {
text: 'Hello world!',
};
handleChange(e) {
this.setState({text: e.target.value})
}
render() {
return (
<div>
<input
onChange={this.handleChange.bind(this)}
type='text'>
</input>
<h1>{this.state.text}</h1>
</div>
);
}
}
export default SplitRight
export default class DangerouslySet extends React.Component {
state = {
text: '<b>Hello world!</b>',
};
handleChange(e) {
this.setState({text: e.target.value})
}
danger(input) {
return {__html: input}
}
render() {
return (
<div>
<input
onChange={this.handleChange.bind(this)}
type='text'>
</input>
<h1
dangerouslySetInnerHTML=
{this.danger(this.state.text)}
</h1>
</div>
);
}
}
export default SplitRight
DangerouslySetInnerHTML
--- # Who would do that?Signal (Desktop)1
- Electron based application
- Used
dangerouslySetInnerHTML
under the hood2 - The patch:
- <div className="text" dangerouslySetInnerHTML={{ __html: text }} />
+ <div className="text">
+ <MessageBody text={text} />
+ </div>
- Never use
dangerouslySetInnerHTML
with user-supplied input - If absolutely necessary, use iFrame sandboxing via other origins specifically for user-supplied content
- Use DomPurify1
export default class DOMPurify extends React.Component {
state = {
text: '<b>Hello world!</b>',
};
handleChange(e) {
this.setState({text: e.target.value})
}
purify(input) {
return {__html: DOMPurify.sanitize(input)}
}
render() {
return (
<div>
<input
onChange={this.handleChange.bind(this)}
type='text'>
</input>
<h1
dangerouslySetInnerHTML={this.purify(this.state.text)}>
</h1>
</div>
);
}
}
export default SplitRight
DOMPurify
---export default class URLIssue extends React.Component {
state = {
links: []
};
addLink(e) {
this.setState({
links: [...this.state.links, e.target.value]
})
}
render() {
return (
<div>
<h1>Links</h1>
<input
onKeyPress={this.addLink.bind(this)}
type='text'>
</input>
<ul>
{this.state.links.map(link =>
<li><a href={link}>{link}</a></li>
)}
</ul>
</div>
);
}
}
export default SplitRight
export default class URLIssueFixed extends React.Component {
state = {
links: []
};
addLink(e) {
const userSuppliedURL = e.target.value;
const parsed = new URL(userSuppliedURL);
if(parsed.protocol !== "https:") {
return;
}
e.key === 'Enter' ?
this.setState({
links: [...this.state.links, userSuppliedURL]
}) : ''
}
render() {
//render it
}
}
export default SplitRight
Leverage whitelisting to prevent unsafe URI schemes
- You can render React components on the server prior to sending to the client
- Improves response times and user experience
- Allows for code splitting
- Useful for SEO
- But: Increases complexity, adds to attack surface
function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`
}
- May lead to reflected XSS issues in your application 1
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// http://redux.js.org/recipes/ServerRendering.html#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
</script>
- Alternatively, use the
serialize-javascript
package
- Recently patched issue with ReactDOMServer API1:
let props = {};
props[userProvidedData] = "hello";
let element = <div {...props} />;
let html = ReactDOMServer.renderToString(element);
- Previous known issue with
createElement
, no longer works due to patch in 20152:
React.createElement(userInput)
- Welcome to NPM land where 99% of your application is dependencies
- Very tempting to use external components
- Github stars !== "secure"
- Actually a bug found on FB bug bounty1
- Boils down to two things:
- Lack of path normalization
- URLs that conduct sensitive operations on the same origin (via GraphQL in this instance)
Most single page applications contain a function to attach an JWT or CSRF token with each request:
componentWillMount() {
const options = {
headers: { 'Authorization': getToken() }
};
const userSuppliedInput = window.location.hash.substr(1);
fetch(`/sensitive-operation/${userSuppliedInput}`, options);
}
An attacker abuse this:
https://application.com/mycomponent#../../other-operation
Which will send the token with the request
- Leverage whitelisting on user supplied input
- Normalize URLs prior to making the request (especially authenticated requests)
- Leverage CSP with strict-dynamic1
- WebPack has easy integration for SRI (see
webpack-subresource-integrity
)2
const compiler = webpack({
plugins: [
new SriPlugin({
hashFuncNames: ['sha256', 'sha384'],
}),
],
});
- Be careful with
dangerouslySetInnerHTML
- Leverage URL protocol whitelisting
- Double check any third-party components
- Make generous use of
npm audit
for dependencies and keep your dependencies up to date - Leverage linting to detect use of dangerous functions and limit bad coding patterns (e.g.
eslint
)