From a723af2b031d3c5acebe16b868a06d45342bc154 Mon Sep 17 00:00:00 2001
From: Simon Sturmer <sstur@me.com>
Date: Mon, 25 Sep 2017 19:05:48 +0700
Subject: [PATCH] Prohibit unsafe data uri in export also

---
 .../draft-js-export-html/src/stateToHTML.js   | 11 +++++++--
 .../draft-js-export-html/test/test-cases.txt  |  4 ++++
 .../src/stateToMarkdown.js                    |  4 ++++
 .../test/test-cases.txt                       |  4 ++++
 .../src/stateFromElement.js                   | 11 +--------
 .../src/__tests__/isAllowedHref-test.js       | 24 +++++++++++++++++++
 packages/draft-js-utils/src/isAllowedHref.js  | 12 ++++++++++
 packages/draft-js-utils/src/main.js           |  1 +
 8 files changed, 59 insertions(+), 12 deletions(-)
 create mode 100644 packages/draft-js-utils/src/__tests__/isAllowedHref-test.js
 create mode 100644 packages/draft-js-utils/src/isAllowedHref.js

diff --git a/packages/draft-js-export-html/src/stateToHTML.js b/packages/draft-js-export-html/src/stateToHTML.js
index 4b90501a..0e19e899 100644
--- a/packages/draft-js-export-html/src/stateToHTML.js
+++ b/packages/draft-js-export-html/src/stateToHTML.js
@@ -5,6 +5,7 @@ import normalizeAttributes from './helpers/normalizeAttributes';
 import styleToCSS from './helpers/styleToCSS';
 
 import {
+  isAllowedHref,
   getEntityRanges,
   BLOCK_TYPE,
   ENTITY_TYPE,
@@ -84,7 +85,7 @@ const ENTITY_ATTR_MAP: {[entityType: string]: AttrMap} = {
 
 // Map entity data to element attributes.
 const DATA_TO_ATTR = {
-  [ENTITY_TYPE.LINK](entityType: string, entity: EntityInstance): Attributes {
+  [ENTITY_TYPE.LINK](entityType: string, entity: EntityInstance): ?Attributes {
     let attrMap = ENTITY_ATTR_MAP.hasOwnProperty(entityType)
       ? ENTITY_ATTR_MAP[entityType]
       : {};
@@ -94,6 +95,9 @@ const DATA_TO_ATTR = {
       let dataValue = data[dataKey];
       if (attrMap.hasOwnProperty(dataKey)) {
         let attrKey = attrMap[dataKey];
+        if (attrKey === 'href' && !isAllowedHref(dataValue)) {
+          return null;
+        }
         attrs[attrKey] = dataValue;
       } else if (DATA_ATTRIBUTE.test(dataKey)) {
         attrs[dataKey] = dataValue;
@@ -101,7 +105,7 @@ const DATA_TO_ATTR = {
     }
     return attrs;
   },
-  [ENTITY_TYPE.IMAGE](entityType: string, entity: EntityInstance): Attributes {
+  [ENTITY_TYPE.IMAGE](entityType: string, entity: EntityInstance): ?Attributes {
     let attrMap = ENTITY_ATTR_MAP.hasOwnProperty(entityType)
       ? ENTITY_ATTR_MAP[entityType]
       : {};
@@ -403,6 +407,9 @@ class MarkupGenerator {
           let attrs = DATA_TO_ATTR.hasOwnProperty(entityType)
             ? DATA_TO_ATTR[entityType](entityType, entity)
             : null;
+          if (attrs == null) {
+            return content;
+          }
           let attrString = stringifyAttrs(attrs);
           return `<a${attrString}>${content}</a>`;
         } else if (entityType != null && entityType === ENTITY_TYPE.IMAGE) {
diff --git a/packages/draft-js-export-html/test/test-cases.txt b/packages/draft-js-export-html/test/test-cases.txt
index 4a73cd0e..45e89555 100644
--- a/packages/draft-js-export-html/test/test-cases.txt
+++ b/packages/draft-js-export-html/test/test-cases.txt
@@ -26,6 +26,10 @@
 {"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"url":"/","rel":null,"title":"hi","extra":"foo","data-id":42,"data-mutability":"mutable","data-False":"bad","data-":"no"}}},"blocks":[{"key":"8r91j","text":"a","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":1,"style":"ITALIC"}],"entityRanges":[{"offset":0,"length":1,"key":0}]}]}
 <p><a href="/" title="hi" data-id="42" data-mutability="mutable"><em>a</em></a></p>
 
+# Link as text if it has unsafe data URI
+{"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"href":"data:text/html;base64,PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4="}}},"blocks":[{"key":"33nh8","text":"x","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":0,"length":1,"key":0}]}]}
+<p>x</p>
+
 # Entity with inline style
 {"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"url":"/"}}},"blocks":[{"key":"8r91j","text":"a","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":0,"length":1,"style":"ITALIC"}],"entityRanges":[{"offset":0,"length":1,"key":0}]}]}
 <p><a href="/"><em>a</em></a></p>
diff --git a/packages/draft-js-export-markdown/src/stateToMarkdown.js b/packages/draft-js-export-markdown/src/stateToMarkdown.js
index 02275d52..a039bdbe 100644
--- a/packages/draft-js-export-markdown/src/stateToMarkdown.js
+++ b/packages/draft-js-export-markdown/src/stateToMarkdown.js
@@ -1,6 +1,7 @@
 // @flow
 
 import {
+  isAllowedHref,
   getEntityRanges,
   BLOCK_TYPE,
   ENTITY_TYPE,
@@ -216,6 +217,9 @@ class MarkupGenerator {
         let entity = entityKey ? contentState.getEntity(entityKey) : null;
         if (entity != null && entity.getType() === ENTITY_TYPE.LINK) {
           let data = entity.getData();
+          if (!isAllowedHref(data.url)) {
+            return content;
+          }
           let url = data.url || '';
           let title = data.title ? ` "${escapeTitle(data.title)}"` : '';
           return `[${content}](${encodeURL(url)}${title})`;
diff --git a/packages/draft-js-export-markdown/test/test-cases.txt b/packages/draft-js-export-markdown/test/test-cases.txt
index 799d13a0..394e47d7 100644
--- a/packages/draft-js-export-markdown/test/test-cases.txt
+++ b/packages/draft-js-export-markdown/test/test-cases.txt
@@ -38,6 +38,10 @@ Hello [World](/a).
 {"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"url":"/a","title":"f\"oo"}}},"blocks":[{"key":"2m141","text":"Hello World.","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":6,"length":5,"key":0}]}]}
 Hello [World](/a "f\"oo").
 
+>> Link as text if it has unsafe data URI
+{"entityMap":{"0":{"type":"LINK","mutability":"MUTABLE","data":{"url":"data:text/html;base64,PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4="}}},"blocks":[{"key":"2m141","text":"Hello World.","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":6,"length":5,"key":0}]}]}
+Hello World.
+
 >> Ordered List
 {"entityMap":{},"blocks":[{"key":"33nh8","text":"An ordered list:","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[]},{"key":"8kinl","text":"One","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[]},{"key":"ekll4","text":"Two","type":"ordered-list-item","depth":0,"inlineStyleRanges":[],"entityRanges":[]}]}
 An ordered list:
diff --git a/packages/draft-js-import-element/src/stateFromElement.js b/packages/draft-js-import-element/src/stateFromElement.js
index 3fac7735..da8f5c8f 100644
--- a/packages/draft-js-import-element/src/stateFromElement.js
+++ b/packages/draft-js-import-element/src/stateFromElement.js
@@ -3,7 +3,7 @@
 import replaceTextWithMeta from './lib/replaceTextWithMeta';
 import {CharacterMetadata, ContentBlock, ContentState, genKey} from 'draft-js';
 import {List, Map, OrderedSet, Repeat, Seq} from 'immutable';
-import {BLOCK_TYPE, ENTITY_TYPE, INLINE_STYLE} from 'draft-js-utils';
+import {isAllowedHref, BLOCK_TYPE, ENTITY_TYPE, INLINE_STYLE} from 'draft-js-utils';
 import {NODE_TYPE_ELEMENT, NODE_TYPE_TEXT} from 'synthetic-dom';
 import {
   INLINE_ELEMENTS,
@@ -86,7 +86,6 @@ type Options = {
 };
 type DataMap<T> = {[key: string]: T};
 
-const DATA_URL = /^data:/i;
 const NO_STYLE = OrderedSet();
 const NO_ENTITY = null;
 
@@ -559,14 +558,6 @@ function toStringMap(input: mixed) {
   return result;
 }
 
-function isAllowedHref(input: ?string) {
-  if (input == null || input.match(DATA_URL)) {
-    return false;
-  } else {
-    return true;
-  }
-}
-
 export function stateFromElement(
   element: DOMElement,
   options?: Options,
diff --git a/packages/draft-js-utils/src/__tests__/isAllowedHref-test.js b/packages/draft-js-utils/src/__tests__/isAllowedHref-test.js
new file mode 100644
index 00000000..676797c7
--- /dev/null
+++ b/packages/draft-js-utils/src/__tests__/isAllowedHref-test.js
@@ -0,0 +1,24 @@
+// @flow
+import isAllowedHref from '../isAllowedHref';
+
+it('should allow valid URIs', () => {
+  expect(isAllowedHref('/')).toBe(true);
+  expect(isAllowedHref('/a')).toBe(true);
+  expect(isAllowedHref('/a.b')).toBe(true);
+  expect(isAllowedHref('#')).toBe(true);
+  expect(isAllowedHref('#a=1&b=2')).toBe(true);
+  expect(isAllowedHref('http://foo')).toBe(true);
+  expect(isAllowedHref('https://foo')).toBe(true);
+  expect(isAllowedHref('x://y')).toBe(true);
+  expect(isAllowedHref('x:y')).toBe(true);
+});
+
+it('should not allow empty values', () => {
+  expect(isAllowedHref(null)).toBe(false);
+  expect(isAllowedHref(undefined)).toBe(false);
+});
+
+it('should not allow data URIs', () => {
+  expect(isAllowedHref('data:text/html;base64,YWJj')).toBe(false);
+  expect(isAllowedHref('data:x')).toBe(false);
+});
diff --git a/packages/draft-js-utils/src/isAllowedHref.js b/packages/draft-js-utils/src/isAllowedHref.js
new file mode 100644
index 00000000..4fbd8f5c
--- /dev/null
+++ b/packages/draft-js-utils/src/isAllowedHref.js
@@ -0,0 +1,12 @@
+// @flow
+const DATA_URL = /^data:/i;
+
+function isAllowedHref(input: ?string) {
+  if (input == null || input.match(DATA_URL)) {
+    return false;
+  } else {
+    return true;
+  }
+}
+
+export default isAllowedHref;
diff --git a/packages/draft-js-utils/src/main.js b/packages/draft-js-utils/src/main.js
index b69e30a3..441bddc8 100644
--- a/packages/draft-js-utils/src/main.js
+++ b/packages/draft-js-utils/src/main.js
@@ -12,3 +12,4 @@ export {default as selectionContainsEntity} from './selectionContainsEntity';
 export {
   default as callModifierForSelectedBlocks,
 } from './callModifierForSelectedBlocks';
+export {default as isAllowedHref} from './isAllowedHref';