Skip to content

Latest commit

 

History

History
380 lines (306 loc) · 8.08 KB

slides.mdx

File metadata and controls

380 lines (306 loc) · 8.08 KB

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'

Reviewing and Securing React Applications


Bio

  • Amanvir Sangha
  • Security Consultant @ Synopsys SIG
  • Interests: Static Analysis, Application Security

This Talk

  • Quick introduction to React
  • Securing your React application
  • Remediation

What is React?

  • 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

Why React?

  • 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>

Remediation

  • 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


Server-side Rendering

  • 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

Careful with Server-side Rendering!

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

Remediation

<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

Keep the framework updated!

  • 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)

Third-Party Components

  • Welcome to NPM land where 99% of your application is dependencies
  • Very tempting to use external components
  • Github stars !== "secure"

Client-side CSRF

  • 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)

Client-side CSRF

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


Remediation

  • Leverage whitelisting on user supplied input
  • Normalize URLs prior to making the request (especially authenticated requests)

Proactive Measures

  • 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'],
        }),
    ],
});

Conclusion

  • 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)

Thanks! Any questions?

Twitter: @_amanvir

Footnotes

  1. https://thehackerblog.com/i-too-like-to-live-dangerously-accidentally-finding-rce-in-signal-desktop-via-html-injection-in-quoted-replies/ 2 3 4 5 6

  2. https://github.com/signalapp/Signal-Desktop/commit/4e5c8965ff72576a9e20850dd30d9985f4073192 2 3