{"article":{"slug":"rendering-untrusted-html-email-safely","title":"Rendering untrusted HTML email, safely","subtitle":null,"summary":"Close's Phil Freo explains how the sales CRM renders synced, untrusted HTML email safely inside its single-page app entirely on the client side, comparing server-side sanitizing and other approaches to the sandboxed iframe technique it uses, along with handling iframe height and a secure WYSIWYG editor for composing.","content_type":"blog_post","language":"en","canonical_url":"https://making.close.com/posts/rendering-untrusted-html-email-safely/","author":{"name":"Phil Freo","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Close","url":"https://making.close.com/","listing_slug":"close","listing":{"slug":"close","name":"Close","listing_type":"company","url":"https://listedstartups.com/companies/close"}},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Web Development","slug":"web-development","url":"https://listedarticles.com/topics/web-development"},{"name":"Email","slug":"email","url":"https://listedarticles.com/topics/email"},{"name":"Frontend","slug":"frontend","url":"https://listedarticles.com/topics/frontend"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1910,"reading_minutes":8,"published_at":"2021-09-29T00:00:00.000Z","added_at":"2026-10-20T08:10:05.656Z","updated_at":"2026-10-20T08:10:05.656Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/rendering-untrusted-html-email-safely","markdown_url":"https://listedarticles.com/articles/rendering-untrusted-html-email-safely.md","example":false,"citation":"Phil Freo, Close. \"Rendering untrusted HTML email, safely.\" 29 Sept 2021. https://making.close.com/posts/rendering-untrusted-html-email-safely/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://making.close.com/posts/rendering-untrusted-html-email-safely/"},"body_markdown":"# Rendering untrusted HTML email, safely\n\nPhil Freo | The Making of Close | September 29, 2021\n\nOne of the earliest core features we built in Close was our\n[email sync feature](https://close.com/emailing/), which allows Close to\nautomatically pull in email communication between your sales team and your sales\ncontacts and display them for your team to see. We present a unified timeline of\nall communication (emails, calls, SMS, etc.) with a sales lead in one place for\nyour team.\n\nThis feature required that we build a way to display email in our web app –\n*safely*, without security concerns that typically come with naively rendering\nuntrusted HTML\n([Stored XSS Attacks](https://owasp.org/www-community/attacks/xss/#Stored_and_Reflected_XSS_Attacks)).\n\nWe launched our ability to display synced HTML email in 2014, however the\ntechnical side of it is still very relevant and (I think) interesting since our\napproach is not one that I've seen widely used.\n\nIn the end, we found a way to, exclusively on the client-side (in web browsers),\nrender untrusted HTML email safely inside our single-page application (written\noriginally with Backbone.js, now mostly React). Here we'll discuss a few\napproaches we had to consider and how we solved this problem.\n\nFor example, we can display an IMAP-synced email thread like this:\n\n[![email](https://making.close.com/static/59bc283663f231da9d5d10687803846a/fe486/email.png)](https://making.close.com/static/59bc283663f231da9d5d10687803846a/18650/email.png)\n\n# HTML vs Plaintext?\n\nWhile many emails do come with a plaintext variant, we had to immediately\ndismiss the idea of only displaying plaintext email, as convenient as that would\nhave been from a security perspective. Many emails don't have a reasonable\nplaintext alternative, and users simply expect to be able to view HTML formatted\nemail as they were designed.\n\nSome emails *only* have a plaintext variant. In those cases we can render the\nplaintext content pretty simply in our web app: effectively just convert new\nlines into `<br>` tags and convert URLs into links.\n\nBut most of the interesting engineering/security concerns are around rendering\nuntrusted HTML content (in this case, email content) safely, which is where the\nremainder of this blog post will focus.\n\n# Main goals and concerns\n\nOur main goals were the following:\n\n- Display HTML email, not just plaintext\n- Display \"full\" HTML email. We care about much more than just a few simple tags\n  like `<b>` and `<i>`, but rather want to support displaying sophisticated\n  email styles (including custom `style` tags) that you often see in\n  well-designed HTML email newsletters, calendar invitations, etc.\n- We want to render emails within our web application, without any conflicts of\n  our CSS leaking in or the email CSS leaking out.\n- In addition to CSS styles, we also want to display images, expect links to\n  work, etc.\n- Most important: Do not let any JavaScript run. Otherwise we run the risk of an\n  attacker taking over our user's Close account by stealing cookies, or a number\n  of other Very Bad™️ things.\n\n# Finding a solution…\n\n## Saying No to HTML Sanitization\n\nWhile there may be some rock-solid libraries out there that can take untrusted\nHTML content and make it completely safe for rendering in HTML, there were a lot\nof potential downsides to this idea.\n\nFirst off, in 2014, we didn't immediately trust any of them. HTML sanitization\nlibraries often rely on parsing HTML with regex, which is a\n[notoriously bad idea](https://stackoverflow.com/a/1732454/137067). We\nabsolutely didn't want to be trying to maintain a library like this ourselves,\nor have to worry about some unhandled edge case that could lead to a major\nsecurity vulnerability.\n\nAdditionally, unlike other use cases (e.g. rendering a blog comment which may\ncontain just some basic markup for bolding text), we felt like we needed to\nrender complex (heavily styled) HTML emails properly, just like you'd see in\nGmail or other modern email clients, so we couldn't default to a tiny whitelist\nof tags or supported attributes.\n\nFinally, HTML sanitization on its own wouldn't be enough, because we want to\nrender the email within an existing web app, and we don't want its styles\naffecting the styles of the rest of the web app. There's a well-known solution\nto this – iframes – that we could have used to help here.\n\nOnce we explored iframes, however, we realized they could do everything we\nneeded already on their own…\n\n## What can iframes do for us?\n\nMost web developers know that an `<iframe>` can render a web page in a fairly\nisolated fashion. Certainly they can solve the issue of our web app styles\nincorrectly affecting an email's styles (or vice versa).\n\nBut we still have that pesky issue of JavaScript.\n\n### Uncommon but helpful iframe attributes\n\nThere are a number of lesser known iframe attributes that can be very helpful.\nLet's take a look at a few.\n\n#### **`srcdoc`**\n\nMost commonly iframes point to a `src` webpage URL – either on the same origin\nor different origin. But since Close is a Single Page Application and our\nfrontend would already be fetching data about the email from our\n[REST API](https://developer.close.com/) (and could include the email body\nitself), we discovered that this attribute allowed us to inject HTML content\ninto an iframe (with all the typical iframe benefits) without needing that\ncontent to be served by a separate page.\n\nSo rather than having to do something like:\n\n`<iframe src=\"/email/body/?email_id=EMAIL_ID_HERE\">`\n\n... which would have required us to setup an additional endpoint, we could\nsimply display the HTML we already had:\n\n`<iframe srcdoc=\"UNTRUSTED_HTML_HERE\">`\n\nNeither approach is particularly more or less secure than the other. Both make\nyou still think about \"what about script execution\" and consider the \"same\norigin\" vs. \"different origin\" options (e.g. we might have needed a separate\ndomain and then have to pass some kind of token for authentication). We chose\n`srcdoc` because it worked and was easiest for our setup.\n\nFor the value that we inject into the `srcdoc`, we actually wrap the HTML email\nin a little HTML, with two main goals:\n\n- Give email bodies some sane default styles, and make them fit into the\n  look-and-feel of our web app by default.\n- Put `target=\"_blank\"` on all links by default (via a `<base>` tag), since we\n  never want a user clicking an email link to load a new webpage in the iframe,\n  and this is also a convenient way for us to apply this behavior to all links\n  without having to traverse the email's HTML.\n- Apply a [`Content-Security-Policy`](https://content-security-policy.com/)\n  `<meta>` tag instructing the iframe document to avoid executing any JavaScript\n  whatsoever. (If we used a hosted page with `src` instead of `srcdoc`, this\n  would be done with an HTTP Header instead).\n\n```\n<html>\n  <head>\n    <meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'none'\" />\n    <base target=\"_blank\" />\n    <style>\n      body {\n        margin: 0;\n        font:\n          13px -apple-system,\n          system-ui,\n          'Segoe UI',\n          Roboto,\n          Oxygen-Sans,\n          Ubuntu,\n          Cantarell,\n          'Helvetica Neue',\n          sans-serif,\n          'Apple Color Emoji',\n          'Segoe UI Emoji',\n          'Segoe UI Symbol',\n          sans-serif;\n        overflow-y: hidden;\n      }\n      html:not(.x),\n      body:not(.x) {\n        height: auto !important;\n      }\n      p:first-child {\n        margin-top: 0;\n      }\n      p:last-child {\n        margin-bottom: 0;\n      }\n      a[href] {\n        color: #3781b8;\n        text-decoration: none;\n      }\n      a[href]:hover {\n        text-decoration: underline;\n      }\n      blockquote[type='cite'] {\n        margin: 0 0 0 0.8ex;\n        border-left: 1px #ccc solid;\n        padding-left: 1ex;\n      }\n      img {\n        max-width: 100%;\n      }\n      ul,\n      ol {\n        padding: 0;\n        margin: 0 0 10px 25px;\n      }\n      ul {\n        list-style-type: disc;\n      }\n    </style>\n  </head>\n  <body>\n    ${bodyHtml}\n  </body>\n</html>\n```\n\n#### **`sandbox`**\n\nThis attribute is the heart of our security model. By adding this attribute, it\nadds a whole bunch of restrictions to the iframe document, including preventing\nany JavaScript from running! Learn more about what's blocked\n[here](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-sandbox)\nand [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe).\nThe `sandbox` attribute allows you to opt back in specific behaviors that are\nblocked by default. We only allow:\n\n- `allow-popups` – We add `target=\"_blank\"` to all of them.\n- `allow-popups-to-escape-sandbox` - So that external webpages opened via a link\n  actually function properly.\n- `allow-same-origin` – We need this for our web app to detect the height of\n  iframe's content. This would be dangerous (and defeat the purpose of\n  `sandbox`) if we also allowed scripts to run by also including\n  `allow-scripts`, however it's harmless on its own.\n\n#### **`csp`**\n\nWe add a Content Security Policy to the iframe with a value of\n`script-src 'none'` as an additional mechanism to avoid any JavaScript from\nrunning in the iframe. This is mostly duplicative with `sandbox` but provides an\nextra layer of protection in\n[browsers](https://caniuse.com/#feat=mdn-api_htmliframeelement_csp) that support\nit.\n\nAs you can see, we have some overlap in coverage here (e.g. 3 separate features\nto avoid JavaScript running). We have learned of particular browser bugs where\nthere were temporary limitations in any one of these indvidiual approaches. A\nreminder that security in layers is often best.\n\n## Bringing it together...\n\n```\n<iframe\n  srcdoc=\"{{UNTRUSTED_HTML_HERE}}\"\n  sandbox=\"allow-popups allow-popups-to-escape-sandbox allow-same-origin\"\n  csp=\"script-src 'none'\"\n/>\n```\n\n## Handling unsupported browsers\n\nAs soon as we made the decision to rely on `<iframe sandbox>` as our primary\nsafety mechanism, we also had to consider users with browsers who didn't have\nthis safety mechanism, so that we didn't put their accounts at risk. Feature\ndetection to the rescue:\n\n```\n// Don't show HTML at all if they are using a browser that doesn't support <iframe sandbox>\n// since our security model relies on it to prevent XSS.\n// (And the basic functionality relies on <iframe srcdoc>)\nconst iframe = document.createElement('iframe');\nconst isHtmlEmailSupported = 'sandbox' in iframe && 'srcdoc' in iframe;\n```\n\nBased on this, we can make sure we *only* display the plaintext email variant\n(if one exists) or else render a message like \"Your browser does not support\ndisplaying email content.\"\n\n## Known limitations\n\nOne can envision additional enhancements where we *would* want to parse and\nmodify the email HTML before rendering it. One example would be if we wanted to\ntry to rewrite and proxy all image URLs\n([similar to Gmail](https://gmail.googleblog.com/2013/12/images-now-showing.html))\nto protect our users' privacy.\n\nSimilarly, we do have a problem where browsers will show a Mixed Content warning\n(since our site is `https://`) after you display an email that includes an\n`<img>` with an `http://` `src`. Those warnings are still fairly unobtrusive,\nhowever, but ultimately we should proxy them to be served over https.\n\nSo while we may one day layer in additional HTML parsing measures (either\nclient-side or server-side), and could decide to switch from an\n`<iframe srcdoc>` to `<iframe src>`, the other security features mentioned here\nwould still likely play an important part of safely rendering HTML email.\n\n## Lots of other details to get right\n\nThere were a lot of other details to get right. Each of them could deserve their\nown blog post, but here is a summary:\n\n- We have to convert \"inline\" images (images that are embedded within the email\n  itself, not referencing an external URL) to be displayable by the web browser.\n- We have to deal with understanding quoting parts. We wrote\n  [quotequail](https://github.com/closeio/quotequail) for this. (Mailgun later\n  open sourced a similar library called\n  [talon](https://github.com/mailgun/talon)). You can see in the screenshot at\n  the top of this post a little \"...\" button where the quoted part of the email\n  is collapsed by default, but expandable.\n- We want to avoid the \"extra scrollbar\" issue you typically experience with\n  iframes, so we carefully (this is surprisingly tricky) detect the height of\n  the iframe email contents and then set the iframe to that height, giving it a\n  seamless look on the page.\n- We need to consider how composing email works too. We needed a WYSIWYG editor\n  for HTML Email that also prevents any XSS. We currently use\n  [Froala Editor](https://froala.com/wysiwyg-editor/) for this, after having\n  tested many editors for security.\n\n## Thoughts?\n\nCan you find any flaws in our email rendering security model? Our product and\napproach has been audited by third party security firms, however we always\nappreciate any reports of security vulnerabilities at\n[security@close.com](mailto:security@close.com).\n\nFeel free to hit me up at [@philfreo](https://twitter.com/philfreo) with any\ncomments or questions!\n\n– Phil Freo\n","body_html":"<h1 id=\"rendering-untrusted-html-email-safely\">Rendering untrusted HTML email, safely</h1>\n<p>Phil Freo | The Making of Close | September 29, 2021</p>\n<p>One of the earliest core features we built in Close was our\n<a href=\"https://close.com/emailing/\" rel=\"nofollow ugc noopener\">email sync feature</a>, which allows Close to\nautomatically pull in email communication between your sales team and your sales\ncontacts and display them for your team to see. We present a unified timeline of\nall communication (emails, calls, SMS, etc.) with a sales lead in one place for\nyour team.</p>\n<p>This feature required that we build a way to display email in our web app –\n<em>safely</em>, without security concerns that typically come with naively rendering\nuntrusted HTML\n(<a href=\"https://owasp.org/www-community/attacks/xss/#Stored_and_Reflected_XSS_Attacks\" rel=\"nofollow ugc noopener\">Stored XSS Attacks</a>).</p>\n<p>We launched our ability to display synced HTML email in 2014, however the\ntechnical side of it is still very relevant and (I think) interesting since our\napproach is not one that I&#39;ve seen widely used.</p>\n<p>In the end, we found a way to, exclusively on the client-side (in web browsers),\nrender untrusted HTML email safely inside our single-page application (written\noriginally with Backbone.js, now mostly React). Here we&#39;ll discuss a few\napproaches we had to consider and how we solved this problem.</p>\n<p>For example, we can display an IMAP-synced email thread like this:</p>\n<p><a href=\"https://making.close.com/static/59bc283663f231da9d5d10687803846a/18650/email.png\" rel=\"nofollow ugc noopener\"><img src=\"https://making.close.com/static/59bc283663f231da9d5d10687803846a/fe486/email.png\" alt=\"email\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<h1 id=\"html-vs-plaintext\">HTML vs Plaintext?</h1>\n<p>While many emails do come with a plaintext variant, we had to immediately\ndismiss the idea of only displaying plaintext email, as convenient as that would\nhave been from a security perspective. Many emails don&#39;t have a reasonable\nplaintext alternative, and users simply expect to be able to view HTML formatted\nemail as they were designed.</p>\n<p>Some emails <em>only</em> have a plaintext variant. In those cases we can render the\nplaintext content pretty simply in our web app: effectively just convert new\nlines into <code>&lt;br&gt;</code> tags and convert URLs into links.</p>\n<p>But most of the interesting engineering/security concerns are around rendering\nuntrusted HTML content (in this case, email content) safely, which is where the\nremainder of this blog post will focus.</p>\n<h1 id=\"main-goals-and-concerns\">Main goals and concerns</h1>\n<p>Our main goals were the following:</p>\n<ul><li>Display HTML email, not just plaintext</li><li><p>Display &quot;full&quot; HTML email. We care about much more than just a few simple tags</p><p>like <code>&lt;b&gt;</code> and <code>&lt;i&gt;</code>, but rather want to support displaying sophisticated\nemail styles (including custom <code>style</code> tags) that you often see in\nwell-designed HTML email newsletters, calendar invitations, etc.</p></li><li><p>We want to render emails within our web application, without any conflicts of</p><p>our CSS leaking in or the email CSS leaking out.</p></li><li><p>In addition to CSS styles, we also want to display images, expect links to</p><p>work, etc.</p></li><li><p>Most important: Do not let any JavaScript run. Otherwise we run the risk of an</p><p>attacker taking over our user&#39;s Close account by stealing cookies, or a number\nof other Very Bad™️ things.</p></li></ul>\n<h1 id=\"finding-a-solution\">Finding a solution…</h1>\n<h2 id=\"saying-no-to-html-sanitization\">Saying No to HTML Sanitization</h2>\n<p>While there may be some rock-solid libraries out there that can take untrusted\nHTML content and make it completely safe for rendering in HTML, there were a lot\nof potential downsides to this idea.</p>\n<p>First off, in 2014, we didn&#39;t immediately trust any of them. HTML sanitization\nlibraries often rely on parsing HTML with regex, which is a\n<a href=\"https://stackoverflow.com/a/1732454/137067\" rel=\"nofollow ugc noopener\">notoriously bad idea</a>. We\nabsolutely didn&#39;t want to be trying to maintain a library like this ourselves,\nor have to worry about some unhandled edge case that could lead to a major\nsecurity vulnerability.</p>\n<p>Additionally, unlike other use cases (e.g. rendering a blog comment which may\ncontain just some basic markup for bolding text), we felt like we needed to\nrender complex (heavily styled) HTML emails properly, just like you&#39;d see in\nGmail or other modern email clients, so we couldn&#39;t default to a tiny whitelist\nof tags or supported attributes.</p>\n<p>Finally, HTML sanitization on its own wouldn&#39;t be enough, because we want to\nrender the email within an existing web app, and we don&#39;t want its styles\naffecting the styles of the rest of the web app. There&#39;s a well-known solution\nto this – iframes – that we could have used to help here.</p>\n<p>Once we explored iframes, however, we realized they could do everything we\nneeded already on their own…</p>\n<h2 id=\"what-can-iframes-do-for-us\">What can iframes do for us?</h2>\n<p>Most web developers know that an <code>&lt;iframe&gt;</code> can render a web page in a fairly\nisolated fashion. Certainly they can solve the issue of our web app styles\nincorrectly affecting an email&#39;s styles (or vice versa).</p>\n<p>But we still have that pesky issue of JavaScript.</p>\n<h3 id=\"uncommon-but-helpful-iframe-attributes\">Uncommon but helpful iframe attributes</h3>\n<p>There are a number of lesser known iframe attributes that can be very helpful.\nLet&#39;s take a look at a few.</p>\n<h4 id=\"srcdoc\"><strong><code>srcdoc</code></strong></h4>\n<p>Most commonly iframes point to a <code>src</code> webpage URL – either on the same origin\nor different origin. But since Close is a Single Page Application and our\nfrontend would already be fetching data about the email from our\n<a href=\"https://developer.close.com/\" rel=\"nofollow ugc noopener\">REST API</a> (and could include the email body\nitself), we discovered that this attribute allowed us to inject HTML content\ninto an iframe (with all the typical iframe benefits) without needing that\ncontent to be served by a separate page.</p>\n<p>So rather than having to do something like:</p>\n<p><code>&lt;iframe src=&quot;/email/body/?email_id=EMAIL_ID_HERE&quot;&gt;</code></p>\n<p>... which would have required us to setup an additional endpoint, we could\nsimply display the HTML we already had:</p>\n<p><code>&lt;iframe srcdoc=&quot;UNTRUSTED_HTML_HERE&quot;&gt;</code></p>\n<p>Neither approach is particularly more or less secure than the other. Both make\nyou still think about &quot;what about script execution&quot; and consider the &quot;same\norigin&quot; vs. &quot;different origin&quot; options (e.g. we might have needed a separate\ndomain and then have to pass some kind of token for authentication). We chose\n<code>srcdoc</code> because it worked and was easiest for our setup.</p>\n<p>For the value that we inject into the <code>srcdoc</code>, we actually wrap the HTML email\nin a little HTML, with two main goals:</p>\n<ul><li><p>Give email bodies some sane default styles, and make them fit into the</p><p>look-and-feel of our web app by default.</p></li><li><p>Put <code>target=&quot;_blank&quot;</code> on all links by default (via a <code>&lt;base&gt;</code> tag), since we</p><p>never want a user clicking an email link to load a new webpage in the iframe,\nand this is also a convenient way for us to apply this behavior to all links\nwithout having to traverse the email&#39;s HTML.</p></li><li><p>Apply a <a href=\"https://content-security-policy.com/\" rel=\"nofollow ugc noopener\"><code>Content-Security-Policy</code></a></p><p><code>&lt;meta&gt;</code> tag instructing the iframe document to avoid executing any JavaScript\nwhatsoever. (If we used a hosted page with <code>src</code> instead of <code>srcdoc</code>, this\nwould be done with an HTTP Header instead).</p></li></ul>\n<pre><code>&lt;html&gt;\n  &lt;head&gt;\n    &lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;script-src &#39;none&#39;&quot; /&gt;\n    &lt;base target=&quot;_blank&quot; /&gt;\n    &lt;style&gt;\n      body {\n        margin: 0;\n        font:\n          13px -apple-system,\n          system-ui,\n          &#39;Segoe UI&#39;,\n          Roboto,\n          Oxygen-Sans,\n          Ubuntu,\n          Cantarell,\n          &#39;Helvetica Neue&#39;,\n          sans-serif,\n          &#39;Apple Color Emoji&#39;,\n          &#39;Segoe UI Emoji&#39;,\n          &#39;Segoe UI Symbol&#39;,\n          sans-serif;\n        overflow-y: hidden;\n      }\n      html:not(.x),\n      body:not(.x) {\n        height: auto !important;\n      }\n      p:first-child {\n        margin-top: 0;\n      }\n      p:last-child {\n        margin-bottom: 0;\n      }\n      a[href] {\n        color: #3781b8;\n        text-decoration: none;\n      }\n      a[href]:hover {\n        text-decoration: underline;\n      }\n      blockquote[type=&#39;cite&#39;] {\n        margin: 0 0 0 0.8ex;\n        border-left: 1px #ccc solid;\n        padding-left: 1ex;\n      }\n      img {\n        max-width: 100%;\n      }\n      ul,\n      ol {\n        padding: 0;\n        margin: 0 0 10px 25px;\n      }\n      ul {\n        list-style-type: disc;\n      }\n    &lt;/style&gt;\n  &lt;/head&gt;\n  &lt;body&gt;\n    ${bodyHtml}\n  &lt;/body&gt;\n&lt;/html&gt;</code></pre>\n<h4 id=\"sandbox\"><strong><code>sandbox</code></strong></h4>\n<p>This attribute is the heart of our security model. By adding this attribute, it\nadds a whole bunch of restrictions to the iframe document, including preventing\nany JavaScript from running! Learn more about what&#39;s blocked\n<a href=\"https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-sandbox\" rel=\"nofollow ugc noopener\">here</a>\nand <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe\" rel=\"nofollow ugc noopener\">here</a>.\nThe <code>sandbox</code> attribute allows you to opt back in specific behaviors that are\nblocked by default. We only allow:</p>\n<ul><li><code>allow-popups</code> – We add <code>target=&quot;_blank&quot;</code> to all of them.</li><li><p><code>allow-popups-to-escape-sandbox</code> - So that external webpages opened via a link</p><p>actually function properly.</p></li><li><p><code>allow-same-origin</code> – We need this for our web app to detect the height of</p><p>iframe&#39;s content. This would be dangerous (and defeat the purpose of\n<code>sandbox</code>) if we also allowed scripts to run by also including\n<code>allow-scripts</code>, however it&#39;s harmless on its own.</p></li></ul>\n<h4 id=\"csp\"><strong><code>csp</code></strong></h4>\n<p>We add a Content Security Policy to the iframe with a value of\n<code>script-src &#39;none&#39;</code> as an additional mechanism to avoid any JavaScript from\nrunning in the iframe. This is mostly duplicative with <code>sandbox</code> but provides an\nextra layer of protection in\n<a href=\"https://caniuse.com/#feat=mdn-api_htmliframeelement_csp\" rel=\"nofollow ugc noopener\">browsers</a> that support\nit.</p>\n<p>As you can see, we have some overlap in coverage here (e.g. 3 separate features\nto avoid JavaScript running). We have learned of particular browser bugs where\nthere were temporary limitations in any one of these indvidiual approaches. A\nreminder that security in layers is often best.</p>\n<h2 id=\"bringing-it-together\">Bringing it together...</h2>\n<pre><code>&lt;iframe\n  srcdoc=&quot;{{UNTRUSTED_HTML_HERE}}&quot;\n  sandbox=&quot;allow-popups allow-popups-to-escape-sandbox allow-same-origin&quot;\n  csp=&quot;script-src &#39;none&#39;&quot;\n/&gt;</code></pre>\n<h2 id=\"handling-unsupported-browsers\">Handling unsupported browsers</h2>\n<p>As soon as we made the decision to rely on <code>&lt;iframe sandbox&gt;</code> as our primary\nsafety mechanism, we also had to consider users with browsers who didn&#39;t have\nthis safety mechanism, so that we didn&#39;t put their accounts at risk. Feature\ndetection to the rescue:</p>\n<pre><code>// Don&#39;t show HTML at all if they are using a browser that doesn&#39;t support &lt;iframe sandbox&gt;\n// since our security model relies on it to prevent XSS.\n// (And the basic functionality relies on &lt;iframe srcdoc&gt;)\nconst iframe = document.createElement(&#39;iframe&#39;);\nconst isHtmlEmailSupported = &#39;sandbox&#39; in iframe &amp;&amp; &#39;srcdoc&#39; in iframe;</code></pre>\n<p>Based on this, we can make sure we <em>only</em> display the plaintext email variant\n(if one exists) or else render a message like &quot;Your browser does not support\ndisplaying email content.&quot;</p>\n<h2 id=\"known-limitations\">Known limitations</h2>\n<p>One can envision additional enhancements where we <em>would</em> want to parse and\nmodify the email HTML before rendering it. One example would be if we wanted to\ntry to rewrite and proxy all image URLs\n(<a href=\"https://gmail.googleblog.com/2013/12/images-now-showing.html\" rel=\"nofollow ugc noopener\">similar to Gmail</a>)\nto protect our users&#39; privacy.</p>\n<p>Similarly, we do have a problem where browsers will show a Mixed Content warning\n(since our site is <code>https://</code>) after you display an email that includes an\n<code>&lt;img&gt;</code> with an <code>http://</code> <code>src</code>. Those warnings are still fairly unobtrusive,\nhowever, but ultimately we should proxy them to be served over https.</p>\n<p>So while we may one day layer in additional HTML parsing measures (either\nclient-side or server-side), and could decide to switch from an\n<code>&lt;iframe srcdoc&gt;</code> to <code>&lt;iframe src&gt;</code>, the other security features mentioned here\nwould still likely play an important part of safely rendering HTML email.</p>\n<h2 id=\"lots-of-other-details-to-get-right\">Lots of other details to get right</h2>\n<p>There were a lot of other details to get right. Each of them could deserve their\nown blog post, but here is a summary:</p>\n<ul><li><p>We have to convert &quot;inline&quot; images (images that are embedded within the email</p><p>itself, not referencing an external URL) to be displayable by the web browser.</p></li><li><p>We have to deal with understanding quoting parts. We wrote</p><p><a href=\"https://github.com/closeio/quotequail\" rel=\"nofollow ugc noopener\">quotequail</a> for this. (Mailgun later\nopen sourced a similar library called\n<a href=\"https://github.com/mailgun/talon\" rel=\"nofollow ugc noopener\">talon</a>). You can see in the screenshot at\nthe top of this post a little &quot;...&quot; button where the quoted part of the email\nis collapsed by default, but expandable.</p></li><li><p>We want to avoid the &quot;extra scrollbar&quot; issue you typically experience with</p><p>iframes, so we carefully (this is surprisingly tricky) detect the height of\nthe iframe email contents and then set the iframe to that height, giving it a\nseamless look on the page.</p></li><li><p>We need to consider how composing email works too. We needed a WYSIWYG editor</p><p>for HTML Email that also prevents any XSS. We currently use\n<a href=\"https://froala.com/wysiwyg-editor/\" rel=\"nofollow ugc noopener\">Froala Editor</a> for this, after having\ntested many editors for security.</p></li></ul>\n<h2 id=\"thoughts\">Thoughts?</h2>\n<p>Can you find any flaws in our email rendering security model? Our product and\napproach has been audited by third party security firms, however we always\nappreciate any reports of security vulnerabilities at\n<a href=\"mailto:security@close.com\">security@close.com</a>.</p>\n<p>Feel free to hit me up at <a href=\"https://twitter.com/philfreo\" rel=\"nofollow ugc noopener\">@philfreo</a> with any\ncomments or questions!</p>\n<p>– Phil Freo</p>","headings":[{"level":1,"text":"Rendering untrusted HTML email, safely","id":"rendering-untrusted-html-email-safely"},{"level":1,"text":"HTML vs Plaintext?","id":"html-vs-plaintext"},{"level":1,"text":"Main goals and concerns","id":"main-goals-and-concerns"},{"level":1,"text":"Finding a solution…","id":"finding-a-solution"},{"level":2,"text":"Saying No to HTML Sanitization","id":"saying-no-to-html-sanitization"},{"level":2,"text":"What can iframes do for us?","id":"what-can-iframes-do-for-us"},{"level":3,"text":"Uncommon but helpful iframe attributes","id":"uncommon-but-helpful-iframe-attributes"},{"level":2,"text":"Bringing it together...","id":"bringing-it-together"},{"level":2,"text":"Handling unsupported browsers","id":"handling-unsupported-browsers"},{"level":2,"text":"Known limitations","id":"known-limitations"},{"level":2,"text":"Lots of other details to get right","id":"lots-of-other-details-to-get-right"},{"level":2,"text":"Thoughts?","id":"thoughts"}]}}