6 Commits 340792f26f ... 6c9a17d49e

Author SHA1 Message Date
  Alyssa Rosenzweig 6c9a17d49e sanitize: Permit mailto 5 years ago
  Alyssa Rosenzweig f9bd95faee sanitize: Permit br 5 years ago
  Alyssa Rosenzweig 9f23d09c84 sanitize: Don't be deceptive about links 5 years ago
  Alyssa Rosenzweig 7ad6ed5971 Don't even display the bad tags 5 years ago
  Alyssa Rosenzweig 12a7ada2d2 Factor out addSanitizedChildren 5 years ago
  Alyssa Rosenzweig 71cfb5674d Allow strong/em through 5 years ago
1 changed files with 58 additions and 11 deletions
  1. 58 11
      src/sanitize.js

+ 58 - 11
src/sanitize.js

@@ -2,6 +2,7 @@
  * Sapphire
  *
  * Copyright (C) 2018 eq
+ * Copyright (C) 2018 Alyssa Rosenzweig
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,12 +23,15 @@
 const sanitizationConfig = {
     allowedElements: {
         b: {},
+        strong: {},
         i: {},
+        em: {},
         u: {},
         s: {},
-        a: {href: 'protocol'},
+        br: {},
+        a: {href: 'href'},
     },
-    allowedProtocols: ['http', 'https', 'gopher']
+    allowedProtocols: ['http', 'https', 'mailto', 'gopher']
 };
 
 const isProtocolSafe = function(str) {
@@ -35,29 +39,74 @@ const isProtocolSafe = function(str) {
     return s.length > 1 && sanitizationConfig.allowedProtocols.includes(s[0]);
 };
 
+const stripProtocol = function(href) {
+    return href.split(':').slice(1).join(':');
+};
+
+const addSanitizedChildren = function(newElement, from) {
+    for (const node of from.childNodes) {
+        newElement.appendChild(sanitizeNode(node));
+    }
+};
+
 // TODO: some sort of time limit so bad actors can't DDOS the client with huge
 // element trees
 const sanitizeElement = function(element) {
     const tagName = element.tagName.toLowerCase();
     if (sanitizationConfig.allowedElements[tagName]) {
-        const newElement = document.createElement(tagName);
+        let newElement = document.createElement(tagName);
+
+        /* A link to append to the end */
+        let postfixLink = null;
 
         for (const attr of element.getAttributeNames()) {
             const attrType = sanitizationConfig.allowedElements[tagName][attr];
+            const attrVal = element.getAttribute(attr);
 
             // TODO: other element types
-            if (attrType === 'protocol' && isProtocolSafe(element.getAttribute(attr))) {
-                newElement.setAttribute(attr, element.getAttribute(attr));
+            if (attrType === 'href' && isProtocolSafe(attrVal)) {
+                /* We have a link to a safe protocol. Check if the link text
+                 * matches the destination (safe links), or at least the
+                 * destination with the protocol stripped, common for mailto.
+                 */
+
+                const text = element.innerText;
+
+                if (text == attrVal || text == stripProtocol(attrVal)) {
+                    newElement.setAttribute(attr, attrVal);
+                } else {
+                    /* This is a deceptive link destination, since the text
+                     * doesn't match.  Add a dedicated link to the side. There
+                     * must be no existing properties for this to function */
+
+                    postfixLink = attrVal;
+                    newElement = document.createElement("span");
+                }
             }
         }
 
-        for (const node of element.childNodes) {
-            newElement.appendChild(sanitizeNode(node));
+        addSanitizedChildren(newElement, element);
+
+        if (postfixLink) {
+            /* Append a link destination */
+            const linkEl = document.createElement('a');
+            linkEl.setAttribute('href', postfixLink);
+            linkEl.innerText = postfixLink;
+
+            newElement.appendChild(document.createTextNode(' ('));
+            newElement.appendChild(linkEl);
+            newElement.appendChild(document.createTextNode(')'));
         }
 
         return newElement;
     } else {
-        return document.createTextNode(element.outerHTML);
+        /* The element was not allowed. Strip it, but still allow through its
+         * (sanitized) children */
+
+        const newElement = document.createElement("span");
+        addSanitizedChildren(newElement, element);
+
+        return newElement;
     }
 };
 
@@ -77,9 +126,7 @@ window.sanitizeHtmlString = function(str) {
 
     element.innerHTML = str;
 
-    for (const child of element.childNodes) {
-        newElement.appendChild(sanitizeNode(child));
-    }
+    addSanitizedChildren(newElement, element);
 
     return newElement.innerHTML;
 };