Bug 1411716 - Disallow non http* <link> type urls (eg data: urls). r=mkmelin
authoralta88@gmail.com
Fri, 20 Oct 2017 08:48:16 -0600
changeset 29351 98e543540fb6478308915be0849a5217ad2d0db1
parent 29350 6025ae6cc52479c3c0d99f608513fddd354e8e53
child 29352 3c4fb63620220649e337cf826b3f1afc39824075
push id2068
push userclokep@gmail.com
push dateMon, 13 Nov 2017 19:02:14 +0000
treeherdercomm-beta@9c7e7ce8672b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1411716
Bug 1411716 - Disallow non http* <link> type urls (eg data: urls). r=mkmelin
mailnews/extensions/newsblog/content/FeedUtils.jsm
mailnews/extensions/newsblog/content/feed-parser.js
--- a/mailnews/extensions/newsblog/content/FeedUtils.jsm
+++ b/mailnews/extensions/newsblog/content/FeedUtils.jsm
@@ -600,18 +600,16 @@ var FeedUtils = {
     aFeed.invalidateItems();
     aFeed.removeInvalidItems(true);
     itemds.Flush();
 
     // Update folderpane.
     this.setFolderPaneProperty(aFeed.folder, "favicon", null, "row");
     // Remove from cache.
     delete this[aFeed.server.serverURI][aFeed.url];
-
-    feed = null;
   },
 
 /**
  * Change an existing feed's url, as identified by FZ_FEED resource in the
  * feeds.rdf subscriptions database.
  *
  * @param  obj aFeed      - the feed object
  * @param  string aNewUrl - new url
--- a/mailnews/extensions/newsblog/content/feed-parser.js
+++ b/mailnews/extensions/newsblog/content/feed-parser.js
@@ -115,17 +115,17 @@ FeedParser.prototype =
     if (this.isPermanentRedirect(aFeed, null, channel, null))
       return [];
 
     let tags = this.childrenByTagNameNS(channel, nsURI, "title");
     aFeed.title = aFeed.title || this.getNodeValue(tags ? tags[0] : null);
     tags = this.childrenByTagNameNS(channel, nsURI, "description");
     aFeed.description = this.getNodeValue(tags ? tags[0] : null);
     tags = this.childrenByTagNameNS(channel, nsURI, "link");
-    aFeed.link = this.getNodeValue(tags ? tags[0] : null);
+    aFeed.link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
 
     if (!(aFeed.title || aFeed.description) || !aFeed.link)
     {
       FeedUtils.log.error("FeedParser.parseAsRSS2: missing mandatory element " +
                           "<title> and <description>, or <link>");
       aFeed.onParseError(aFeed);
       return [];
     }
@@ -149,21 +149,21 @@ FeedParser.prototype =
         continue;
 
       let item = new FeedItem();
       item.feed = aFeed;
       item.enclosures = [];
       item.keywords = [];
 
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origLink");
-      let link = this.getNodeValue(tags ? tags[0] : null);
+      let link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
       if (!link)
       {
         tags = this.childrenByTagNameNS(itemNode, nsURI, "link");
-        link = this.getNodeValue(tags ? tags[0] : null);
+        link = this.validLink(this.getNodeValue(tags ? tags[0] : null));
       }
       tags = this.childrenByTagNameNS(itemNode, nsURI, "guid");
       let guidNode = tags ? tags[0] : null;
 
       let guid;
       let isPermaLink = false;
       if (guidNode)
       {
@@ -188,17 +188,18 @@ FeedParser.prototype =
           {
             isPermaLink = false;
           }
         }
 
         item.id = guid;
       }
 
-      item.url = (guid && isPermaLink) ? guid : link ? link : null;
+      let guidLink = this.validLink(guid);
+      item.url = isPermaLink && guidLink ? guidLink : link ? link : null;
       tags = this.childrenByTagNameNS(itemNode, nsURI, "description");
       item.description = this.getNodeValue(tags ? tags[0] : null);
       tags = this.childrenByTagNameNS(itemNode, nsURI, "title");
       item.title = this.getNodeValue(tags ? tags[0] : null);
       if (!(item.title || item.description))
       {
         FeedUtils.log.info("FeedParser.parseAsRSS2: <item> missing mandatory " +
                            "element, either <title> or <description>; skipping");
@@ -261,46 +262,46 @@ FeedParser.prototype =
 
       // Handle <enclosures> and <media:content>, which may be in a
       // <media:group> (if present).
       tags = this.childrenByTagNameNS(itemNode, nsURI, "enclosure");
       let encUrls = [];
       if (tags)
         for (let tag of tags)
         {
-          let url = (tag.getAttribute("url") || "").trim();
+          let url = this.validLink(tag.getAttribute("url"));
           if (url && !encUrls.includes(url))
           {
             item.enclosures.push(new FeedEnclosure(url,
                                                    tag.getAttribute("type"),
                                                    tag.getAttribute("length")));
             encUrls.push(url);
           }
         }
 
       tags = itemNode.getElementsByTagNameNS(FeedUtils.MRSS_NS, "content");
       if (tags)
         for (let tag of tags)
         {
-          let url = (tag.getAttribute("url") || "").trim();
+          let url = this.validLink(tag.getAttribute("url"));
           if (url && !encUrls.includes(url))
             item.enclosures.push(new FeedEnclosure(url,
                                                    tag.getAttribute("type"),
                                                    tag.getAttribute("fileSize")));
         }
 
       // The <origEnclosureLink> tag has no specification, especially regarding
       // whether more than one tag is allowed and, if so, how tags would
       // relate to previously declared (and well specified) enclosure urls.
       // The common usage is to include 1 origEnclosureLink, in addition to
       // the specified enclosure tags for 1 enclosure. Thus, we will replace the
       // first enclosure's, if found, url with the first <origEnclosureLink>
       // url only or else add the <origEnclosureLink> url.
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origEnclosureLink");
-      let origEncUrl = this.getNodeValue(tags ? tags[0] : null);
+      let origEncUrl = this.validLink(this.getNodeValue(tags ? tags[0] : null));
       if (origEncUrl)
       {
         if (item.enclosures.length)
           item.enclosures[0].mURL = origEncUrl;
         else
           item.enclosures.push(new FeedEnclosure(origEncUrl));
       }
 
@@ -345,17 +346,17 @@ FeedParser.prototype =
     if (this.isPermanentRedirect(aFeed, null, channel, ds))
       return [];
 
     aFeed.title = aFeed.title ||
                   this.getRDFTargetValue(ds, channel, FeedUtils.RSS_TITLE) ||
                   aFeed.url;
     aFeed.description = this.getRDFTargetValue(ds, channel, FeedUtils.RSS_DESCRIPTION) ||
                         "";
-    aFeed.link = this.getRDFTargetValue(ds, channel, FeedUtils.RSS_LINK) ||
+    aFeed.link = this.validLink(this.getRDFTargetValue(ds, channel, FeedUtils.RSS_LINK)) ||
                  aFeed.url;
 
     if (!(aFeed.title || aFeed.description) || !aFeed.link)
     {
       FeedUtils.log.error("FeedParser.parseAsRSS1: missing mandatory element " +
                           "<title> and <description>, or <link>");
       aFeed.onParseError(aFeed);
       return [];
@@ -375,33 +376,34 @@ FeedParser.prototype =
     {
       let itemResource = items.getNext().QueryInterface(Ci.nsIRDFResource);
       let item = new FeedItem();
       item.feed = aFeed;
 
       // Prefer the value of the link tag to the item URI since the URI could be
       // a relative URN.
       let uri = itemResource.ValueUTF8;
-      let link = this.getRDFTargetValue(ds, itemResource, FeedUtils.RSS_LINK);
+      let link = this.validLink(this.getRDFTargetValue(ds, itemResource, FeedUtils.RSS_LINK));
       item.url = link || uri;
       item.description = this.getRDFTargetValue(ds, itemResource,
                                                 FeedUtils.RSS_DESCRIPTION);
       item.title = this.getRDFTargetValue(ds, itemResource, FeedUtils.RSS_TITLE) ||
                    this.getRDFTargetValue(ds, itemResource, FeedUtils.DC_SUBJECT) ||
                    (item.description ?
                      (this.stripTags(item.description).substr(0, 150)) : null);
       if (!item.url || !item.title)
       {
         FeedUtils.log.info("FeedParser.parseAsRSS1: <item> missing mandatory " +
                            "element <item rdf:about> and <link>, or <title> and " +
                            "no <description>; skipping");
         continue;
       }
 
       item.id = item.url;
+      item.url = this.validLink(item.url);
 
       item.author = this.getRDFTargetValue(ds, itemResource, FeedUtils.DC_CREATOR) ||
                     this.getRDFTargetValue(ds, channel, FeedUtils.DC_CREATOR) ||
                     aFeed.title ||
                     item.author;
       item.date = this.getRDFTargetValue(ds, itemResource, FeedUtils.DC_DATE) ||
                   item.date;
       item.content = this.getRDFTargetValue(ds, itemResource,
@@ -430,17 +432,17 @@ FeedParser.prototype =
       return [];
 
     let tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "title");
     aFeed.title = aFeed.title ||
                   this.stripTags(this.getNodeValue(tags ? tags[0] : null));
     tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "tagline");
     aFeed.description = this.getNodeValue(tags ? tags[0] : null);
     tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "link");
-    aFeed.link = this.findAtomLink("alternate", tags);
+    aFeed.link = this.validLink(this.findAtomLink("alternate", tags));
 
     if (!aFeed.title)
     {
       FeedUtils.log.error("FeedParser.parseAsAtom: missing mandatory element " +
                           "<title>");
       aFeed.onParseError(aFeed);
       return [];
     }
@@ -460,17 +462,17 @@ FeedParser.prototype =
     {
       if (!itemNode.childElementCount)
         continue;
 
       let item = new FeedItem();
       item.feed = aFeed;
 
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "link");
-      item.url = this.findAtomLink("alternate", tags);
+      item.url = this.validLink(this.findAtomLink("alternate", tags));
 
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "id");
       item.id = this.getNodeValue(tags ? tags[0] : null);
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "summary");
       item.description = this.getNodeValue(tags ? tags[0] : null);
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_03_NS, "title");
       item.title = this.getNodeValue(tags ? tags[0] : null) ||
                    (item.description ? item.description.substr(0, 150) : null);
@@ -575,16 +577,17 @@ FeedParser.prototype =
 
     tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "subtitle");
     aFeed.description = this.serializeTextConstruct(tags ? tags[0] : null);
 
     // Per spec, aFeed.link and contentBase may both end up null here.
     tags = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "link");
     aFeed.link = this.findAtomLink("self", tags, contentBase) ||
                  this.findAtomLink("alternate", tags, contentBase);
+    aFeed.link = this.validLink(aFeed.link);
     if (!contentBase)
       contentBase = aFeed.link;
 
     if (!aFeed.title)
     {
       FeedUtils.log.error("FeedParser.parseAsAtomIETF: missing mandatory element " +
                           "<title>");
       aFeed.onParseError(aFeed);
@@ -616,21 +619,22 @@ FeedParser.prototype =
 
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "source");
       let source = tags ? tags[0] : null;
 
       // Per spec, item.link and contentBase may both end up null here.
       // If <content> is also not present, then <link rel="alternate"> is MUST
       // but we're lenient.
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origLink");
-      item.url = this.getNodeValue(tags ? tags[0] : null);
+      item.url = this.validLink(this.getNodeValue(tags ? tags[0] : null));
       if (!item.url)
       {
         tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "link");
-        item.url = this.findAtomLink("alternate", tags, contentBase) || aFeed.link;
+        item.url = this.validLink(this.findAtomLink("alternate", tags, contentBase)) ||
+                   aFeed.link;
       }
       if (!contentBase)
         contentBase = item.url;
 
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "id");
       item.id = this.getNodeValue(tags ? tags[0] : null);
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "summary");
       item.description = this.serializeTextConstruct(tags ? tags[0] : null);
@@ -704,28 +708,29 @@ FeedParser.prototype =
       // Handle <link rel="enclosure"> (if present).
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.ATOM_IETF_NS, "link");
       let encUrls = [];
       if (tags)
         for (let tag of tags)
         {
           let url = tag.getAttribute("rel") == "enclosure" ?
                       (tag.getAttribute("href") || "").trim() : null;
+          url = this.validLink(url);
           if (url && !encUrls.includes(url))
           {
             item.enclosures.push(new FeedEnclosure(url,
                                                    tag.getAttribute("type"),
                                                    tag.getAttribute("length"),
                                                    tag.getAttribute("title")));
             encUrls.push(url);
           }
         }
 
       tags = this.childrenByTagNameNS(itemNode, FeedUtils.FEEDBURNER_NS, "origEnclosureLink");
-      let origEncUrl = this.getNodeValue(tags ? tags[0] : null);
+      let origEncUrl = this.validLink(this.getNodeValue(tags ? tags[0] : null));
       if (origEncUrl)
       {
         if (item.enclosures.length)
           item.enclosures[0].mURL = origEncUrl;
         else
           item.enclosures.push(new FeedEnclosure(origEncUrl));
       }
 
@@ -887,28 +892,45 @@ FeedParser.prototype =
     return null;
   },
 
   // Finds elements that are direct children of the first arg.
   childrenByTagNameNS: function(aElement, aNamespace, aTagName)
   {
     if (!aElement)
       return null;
+
     let matches = aElement.getElementsByTagNameNS(aNamespace, aTagName);
     let matchingChildren = new Array();
     for (let match of matches)
     {
       if (match.parentNode == aElement)
         matchingChildren.push(match)
     }
 
     return matchingChildren.length ? matchingChildren : null;
   },
 
   /**
+   * Ensure <link> type tags start with http[s]: for values stored in mail
+   * headers (content-base and remote enclosures), particularly to prevent
+   * data: uris, javascript, and other spoofing.
+   *
+   * @param {String} link - An intended http url string.
+   * @return {String}     - A clean string starting with http, else null.
+   */
+  validLink: function(link)
+  {
+    if (/https?:/.test(link))
+      return this.removeUnprintableASCII(link.trim());
+
+    return null;
+  },
+
+  /**
    * Return an absolute link for <entry> relative links. If xml:base is
    * present in a <feed> attribute or child <link> element attribute, use it;
    * otherwise the Feed.link will be the relevant <feed> child <link> value
    * and will be the |baseURI| for <entry> child <link>s if there is no further
    * xml:base, which may be an attribute of any element.
    *
    * @param string linkRel         - the <link> rel attribute value to find.
    * @param NodeList linkElements  - the nodelist of <links> to search in.
@@ -994,16 +1016,27 @@ FeedParser.prototype =
       return;
 
     options.updates.updatePeriod = updatePeriod;
     options.updates.updateFrequency = updateFrequency;
     options.updates.updateBase = updateBase;
     aFeed.options = options;
   },
 
+  /**
+   * Remove unprintable ascii, particularly CR/LF, for non formatted tag values.
+   *
+   * @param {String} s - String to clean.
+   * @return {String}
+   */
+  removeUnprintableASCII: function(s)
+  {
+    return s ? s.replace(/[\x00-\x1F]+/g, "") : "";
+  },
+
   stripTags: function(someHTML)
   {
     return someHTML ? someHTML.replace(/<[^>]+>/g, "") : someHTML;
   },
 
   xmlUnescape: function(s)
   {
     s = s.replace(/&lt;/g, "<");