├── .babelrc ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── react-safe-src-doc-iframe-logo.png └── safe-src-doc-iframe-logo.psd ├── package-lock.json ├── package.json ├── setup-tests.js ├── src ├── __snapshots__ │ └── safe-src-doc-iframe.test.js.snap ├── safe-src-doc-iframe.js └── safe-src-doc-iframe.test.js ├── webpack.dev.config.js ├── webpack.prod.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "umd": { 4 | "presets": ["env", "react"], 5 | "plugins": ["transform-object-rest-spread", "transform-class-properties"] 6 | }, 7 | "es": { 8 | "presets": [ 9 | [ 10 | "env", 11 | { 12 | "modules": false 13 | } 14 | ], 15 | "react" 16 | ], 17 | "plugins": ["transform-object-rest-spread", "transform-class-properties"] 18 | }, 19 | "test": { 20 | "presets": ["env", "react"], 21 | "plugins": ["transform-object-rest-spread", "transform-class-properties"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "jest": true, 6 | "browser": true 7 | }, 8 | "parser": "babel-eslint", 9 | "extends": ["godaddy-react"], 10 | "plugins": ["jsx-a11y"], 11 | "rules": { 12 | "jsx-a11y/iframe-has-title": "error" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | *.log 4 | .DS_Store 5 | .vscode 6 | es 7 | dev 8 | dist 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.1.0] - 2019-04-22 9 | 10 | ### Chore 11 | 12 | - add peer dependency react 16, react dom 16 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 GoDaddy Operating Company, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Safe Srcdoc Iframe 2 | 3 | > A component which applies guards to srcdoc iframes in order to provide a predictable and safe experience to the user. Complements the `sandbox` native iframe attribute. 4 | 5 |

6 | 7 |

8 | 9 | ## Table of Contents 10 | 11 | - [The Problem](#the-problem-) 12 | - [This Solution](#this-solution-) 13 | - [Guards](#guards-) 14 | - [Install](#install) 15 | - [Import Examples](#import-examples) 16 | - [Usage Example](#usage-example-) 17 | - [Component Props](#component-props) 18 | - [referrerPolicy](#referrerpolicy) 19 | - [sandbox](#sandbox) 20 | - [srcDoc](#srcdoc) 21 | - [title](#title) 22 | - [Special Thanks](#special-thanks-) 23 | - [License](#license-) 24 | - [Changelog](/CHANGELOG.md) 25 | - [Security](/SECURITY.md) 26 | 27 | ## The Problem 🔴 28 | 29 | You need to render an html document, for which you have the source locally, into an iframe on your `React` application. However, you would like some guards applied to the document in order to provide a safe and consistent experience to the user. For example, preventing clicks on elements which could lead to page navigation. 30 | 31 | ## This Solution 🔵 32 | 33 | This component compliments the `sandbox` iframe attribute. It will take the html document source code which you provide through the `srcDoc` component prop and inject safeguards on document load. 34 | 35 | ## Guards 👮 36 | 37 | The following guards are applied to the document: 38 | 39 | - disable pointer events on any element with an href attribute, buttons, and images. 40 | - only whitelist the "allow-same-origin" `sandbox` attribute flag for guard injection into the document. 41 | - `referrerPolicy` set to "no-referrer". 42 | 43 | ## Install 44 | 45 | ```npm install --save react-safe-src-doc-iframe``` 46 | 47 | Or 48 | 49 | ```yarn add react-safe-src-doc-iframe``` 50 | 51 | > Note this package also depends on `react` and `prop-types`. Ensure they are installed or available beforehand. 52 | 53 | ### Import Examples 54 | 55 | ```javascript 56 | // 1) es6 module 57 | import SafeSrcDocIframe from 'react-safe-src-doc-iframe'; 58 | 59 | // 2) commonjs 60 | const SafeSrcDocIframe = require('react-safe-src-doc-iframe').default; 61 | 62 | // 3) window 63 | const SafeSrcDocIframe = window.SafeSrcDocIframe; 64 | ``` 65 | 66 | ## Usage Example 📝 67 | 68 | > ▶️ [Try it out on CodeSandbox!](https://codesandbox.io/s/2z4nk0nq4j) 69 | 70 | ```jsx 71 | import React from 'react'; 72 | import SafeSrcDocIframe from 'react-safe-src-doc-iframe'; 73 | 74 | const html = ` 75 | 76 | 77 | 78 | My Cats Page 79 | 80 | 81 | About
82 |
83 | 84 | 85 | 86 | `; 87 | 88 | const App = () => { 89 | return ; 90 | }; 91 | ``` 92 | 93 | ## Component Props 94 | 95 | > Note: any prop not specified here will be forwarded to the native iframe element. **However**, if `src` is passed, it will always be filtered out. 96 | 97 | ### referrerPolicy 98 | 99 | > `string` | optional. Default value: `no-referrer`. 100 | 101 | ### sandbox 102 | 103 | > `string` | optional. Default value: `allow-same-origin` (for safeguard injection). 104 | 105 | The value for the `sandbox` iframe attribute. 106 | 107 | ### srcDoc 108 | 109 | > `string` | required. 110 | 111 | Source of the html document to render. 112 | 113 | ### title 114 | 115 | > `string` | required. 116 | 117 | Provide a title for the iframe in order to help screen reader users. [More info](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/iframe-has-title.md) 118 | 119 | ## Special Thanks 👏 120 | 121 | - ["guard" icon](https://thenounproject.com/search/?q=guard&i=225519) created by Jason Gray, from [the Noun Project](https://thenounproject.com/). Used with attribution under Creative Commons. 122 | - ["Browser" icon](https://thenounproject.com/search/?q=browser&i=1850566) created by Wira, from [the Noun Project](https://thenounproject.com/). Used with attribution under Creative Commons. 123 | - [Downshift for the README format inspiration :)](https://github.com/paypal/downshift) 124 | 125 | ## License 📋 126 | 127 | [MIT](LICENSE) 128 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take security very seriously at GoDaddy. We appreciate your efforts to 4 | responsibly disclose your findings, and will make every effort to acknowledge 5 | your contributions. 6 | 7 | ## Where should I report security issues? 8 | 9 | In order to give the community time to respond and upgrade, we strongly urge you 10 | report all security issues privately. 11 | 12 | To report a security issue in one of our Open Source projects email us directly 13 | at **oss@godaddy.com** and include the word "SECURITY" in the subject line. 14 | 15 | This mail is delivered to our Open Source Security team. 16 | 17 | After the initial reply to your report, the team will keep you informed of the 18 | progress being made towards a fix and announcement, and may ask for additional 19 | information or guidance. -------------------------------------------------------------------------------- /assets/react-safe-src-doc-iframe-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy/react-safe-src-doc-iframe/a6e6493a4531a142ba7395c5042b971cc564b593/assets/react-safe-src-doc-iframe-logo.png -------------------------------------------------------------------------------- /assets/safe-src-doc-iframe-logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy/react-safe-src-doc-iframe/a6e6493a4531a142ba7395c5042b971cc564b593/assets/safe-src-doc-iframe-logo.psd -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-safe-src-doc-iframe", 3 | "version": "1.1.0", 4 | "description": "A component which applies guards to srcdoc iframes, providing a predictable and safe experience to the user.", 5 | "license": "MIT", 6 | "main": "dist/safe-src-doc-iframe.js", 7 | "browser": "dist/safe-src-doc-iframe.js", 8 | "module": "es/safe-src-doc-iframe.js", 9 | "scripts": { 10 | "lint": "eslint --fix --ext jsx --ext js src/", 11 | "test": "npm run lint && NODE_ENV=test jest src/", 12 | "build:dev": "BABEL_ENV=umd webpack --config webpack.dev.config.js", 13 | "build:dist": "BABEL_ENV=umd webpack --config webpack.prod.config.js", 14 | "build:es": "BABEL_ENV=es babel src --out-dir es --source-maps inline", 15 | "build-all": "npm run build:dev && npm run build:dist && npm run build:es", 16 | "prepublishOnly": "npm run build-all" 17 | }, 18 | "files": [ 19 | "src", 20 | "dist", 21 | "es" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/godaddy/react-safe-src-doc-iframe.git" 26 | }, 27 | "keywords": [ 28 | "react", 29 | "react component", 30 | "component", 31 | "iframe", 32 | "safety", 33 | "safe guards", 34 | "srcdoc", 35 | "library" 36 | ], 37 | "author": "GoDaddy Operating Company, LLC", 38 | "contributors": [ 39 | "Mayank Jethva ", 40 | "Srinath Ganesan ", 41 | "Brandon Kidd " 42 | ], 43 | "jest": { 44 | "setupTestFrameworkScriptFile": "setup-tests.js" 45 | }, 46 | "devDependencies": { 47 | "babel-cli": "^6.26.0", 48 | "babel-core": "^6.26.3", 49 | "babel-eslint": "^8.2.6", 50 | "babel-loader": "^7.1.5", 51 | "babel-plugin-transform-class-properties": "^6.24.1", 52 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 53 | "babel-preset-env": "^1.7.0", 54 | "babel-preset-react": "^6.24.1", 55 | "enzyme": "^3.3.0", 56 | "enzyme-adapter-react-16": "^1.1.1", 57 | "eslint": "^5.1.0", 58 | "eslint-config-godaddy-react": "^2.2.1", 59 | "eslint-plugin-json": "^1.2.0", 60 | "eslint-plugin-jsx-a11y": "^6.1.1", 61 | "eslint-plugin-mocha": "^5.1.0", 62 | "eslint-plugin-react": "^7.10.0", 63 | "jest": "^23.4.1", 64 | "react": "^16.4.1", 65 | "react-dom": "^16.4.1", 66 | "webpack": "^4.16.1", 67 | "webpack-cli": "^3.0.8" 68 | }, 69 | "peerDependencies": { 70 | "prop-types": "^15.0.0", 71 | "react": "^15.0.0 || ^16.0.0", 72 | "react-dom": "^15.0.0 || ^16.0.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /setup-tests.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/__snapshots__/safe-src-doc-iframe.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders as expected 1`] = ` 4 | ShallowWrapper { 5 | "length": 1, 6 | Symbol(enzyme.__root__): [Circular], 7 | Symbol(enzyme.__unrendered__): , 22 | Symbol(enzyme.__renderer__): Object { 23 | "batchedUpdates": [Function], 24 | "getNode": [Function], 25 | "render": [Function], 26 | "simulateEvent": [Function], 27 | "unmount": [Function], 28 | }, 29 | Symbol(enzyme.__node__): Object { 30 | "instance": null, 31 | "key": undefined, 32 | "nodeType": "host", 33 | "props": Object { 34 | "referrerPolicy": "no-referrer", 35 | "sandbox": "allow-same-origin", 36 | "srcDoc": " 37 | 38 | 39 | 40 | 41 | mock html page 42 | 43 | 44 | ", 45 | "title": "some-title", 46 | }, 47 | "ref": [Function], 48 | "rendered": null, 49 | "type": "iframe", 50 | }, 51 | Symbol(enzyme.__nodes__): Array [ 52 | Object { 53 | "instance": null, 54 | "key": undefined, 55 | "nodeType": "host", 56 | "props": Object { 57 | "referrerPolicy": "no-referrer", 58 | "sandbox": "allow-same-origin", 59 | "srcDoc": " 60 | 61 | 62 | 63 | 64 | mock html page 65 | 66 | 67 | ", 68 | "title": "some-title", 69 | }, 70 | "ref": [Function], 71 | "rendered": null, 72 | "type": "iframe", 73 | }, 74 | ], 75 | Symbol(enzyme.__options__): Object { 76 | "adapter": ReactSixteenAdapter { 77 | "options": Object { 78 | "enableComponentDidUpdateOnSetState": true, 79 | }, 80 | }, 81 | }, 82 | } 83 | `; 84 | -------------------------------------------------------------------------------- /src/safe-src-doc-iframe.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const disableStylesRaw = ` 5 | *[href], button, img { 6 | pointer-events: none !important; 7 | display: inline-block !important; 8 | } 9 | `; 10 | 11 | class SafesrcDocIframe extends Component { 12 | 13 | static propTypes = { 14 | title: PropTypes.string.isRequired, 15 | srcDoc: PropTypes.string.isRequired, 16 | sandbox: PropTypes.string, 17 | referrerPolicy: PropTypes.string, 18 | src: PropTypes.string 19 | }; 20 | 21 | static defaultProps = { 22 | // set all restrictions for sandbox except same origin 23 | // to allow us to inject the safe guards. 24 | sandbox: 'allow-same-origin', 25 | referrerPolicy: 'no-referrer', 26 | // will be omitted from props passed to the iframe 27 | src: '' 28 | }; 29 | 30 | constructor(...args) { 31 | super(...args); 32 | this.iframeElement = null; 33 | this.disableStyleTag = document.createElement('style'); 34 | this.disableStylesTextNode = document.createTextNode(disableStylesRaw); 35 | this.disableStyleTag.appendChild(this.disableStylesTextNode); 36 | } 37 | 38 | componentDidMount() { 39 | if (this.iframeElement) { 40 | this.iframeElement.onload = () => { 41 | this.applySafeguards(); 42 | }; 43 | } 44 | } 45 | 46 | componentWillUnmount() { 47 | this.disableStylesTextNode = null; 48 | this.disableStyleTag = null; 49 | } 50 | 51 | applySafeguards() { 52 | if (!this.iframeElement.contentDocument) { 53 | return; 54 | } 55 | const [ 56 | iframeBody 57 | ] = this.iframeElement.contentDocument.getElementsByTagName('body'); 58 | if (iframeBody) { 59 | // add safety guards last to ensure they are always applied. 60 | iframeBody.appendChild(this.disableStyleTag); 61 | } 62 | } 63 | 64 | render() { 65 | const { 66 | title, 67 | referrerPolicy, 68 | sandbox, 69 | srcDoc, 70 | src: omit, /* eslint-disable-line no-unused-vars */ 71 | ...rest 72 | } = this.props; 73 | return ( 74 |