RSS is a standardized XML format used to publish and subscribe to frequently updated web content—news headlines, blog posts, announcements, and more. A typical RSS feed contains:
<title>
, <link>
, and <description>
.<title>
, <link>
, and <description>
.XSLT is a language for transforming XML documents into other text formats—typically HTML. By linking an XSLT stylesheet to an RSS feed via:
<?xml-stylesheet type="text/xsl" href="feed.xsl"?>
the browser or client can apply templates to render XML as styled HTML in the browser.
Firefox detects RSS/XML feeds (text/xml
or application/rss+xml
) and applies an internal XSLT that escapes all embedded HTML entities. You see raw tags:
Chromium-based browsers fetch the RSS as XML, apply the linked XSLT, and use disable-output-escaping="yes"
to unescape entities. The result is injected via innerHTML
, rendering the markup:
Here’s a minimal example demonstrating the pivot:
<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="feed.xsl" type="text/xsl"?>
<rss version="2.0">
<channel>
<title>Demo Feed</title>
<item>
<title>XSS Test</title>
<description><h1>Hello User</h1></description>
</item>
</channel>
</rss>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" indent="yes"/>
<xsl:template match="rss/channel">
<h1><xsl:value-of select="title"/></h1>
<xsl:for-each select="item">
<h2><xsl:value-of select="title"/></h2>
<xsl:value-of select="description" disable-output-escaping="yes"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Imagine a web application that accepts user posts via RSS+XSLT as its storage and display mechanism. Contributors submit items, the server writes them to feed.xml
, and clients render the feed directly. Without proper sanitization, any user can embed:
<description><script>stealCookie()</script></description>
In Firefox, those tags appear inert. In Chrome, they execute on page load, stealing sessions or running arbitrary code.
Below is a Flask application that captures posts and writes to an RSS feed file. It includes endpoints to submit new items and view the feed:
from flask import Flask, Response, request
import os
from datetime import datetime
app = Flask(__name__)
temp_file = "temp"
# Ensure temp file exists
def init_temp():
if not os.path.exists(temp_file):
open(temp_file, 'w').close()
@app.route("/", methods=["GET"])
def rss_feed():
init_temp()
# Read any raw XML items from temp
with open(temp_file, 'r', encoding='utf-8') as f:
raw_items = f.read().strip()
rss_content = f"""<?xml version=\"1.0\" encoding=\"utf-8\"?>
<?xml-stylesheet type='text/xsl' href='rss.xsl'?>
<rss version=\"2.0\">
<channel>
<title>Course 1</title>
<link>https://example.com</link>
<description>Course 1 Announcements RSS Feed</description>
<ttl>60</ttl>
{raw_items}
</channel>
</rss>"""
return Response(rss_content, mimetype='text/xml')
@app.route("/rss.xsl", methods=["GET"])
def rss_stylesheet():
xsl_content = """<?xml version=\"1.0\" encoding=\"utf-8\"?>
<xsl:stylesheet version=\"1.0\"
xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">
<xsl:output method=\"html\" indent=\"yes\"/>
<xsl:template match=\"rss/channel\">
<html>
<head>
<title><xsl:value-of select=\"title\"/></title>
</head>
<body>
<h1><xsl:value-of select=\"title\"/></h1>
<div><xsl:value-of select=\"description\"/></div>
<xsl:for-each select=\"item\">
<div style=\"border:1px solid #ccc; margin:1em 0; padding:1em;\">
<h2><xsl:value-of select=\"title\"/></h2>
<xsl:value-of select=\"description\" disable-output-escaping=\"yes\"/>
</div>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>"""
return Response(xsl_content, mimetype='text/xsl')
@app.route("/add", methods=["POST"])
def add_item():
init_temp()
# Expect raw <item>...</item> XML in request body
xml_item = request.data.decode('utf-8').strip()
if xml_item.startswith('<item') and xml_item.endswith('</item>'):
with open(temp_file, 'a', encoding='utf-8') as f:
f.write(xml_item + '\n')
return Response('Item added', status=201)
else:
return Response('Invalid item XML', status=400)
if __name__ == "__main__":
app.run(debug=True)
This video demonstrates how RSS feeds are rendered differently in Chrome and Firefox when HTML entities are present in the XML description field.
description
.script-src 'self'
, block inline execution.disable-output-escaping
or sanitize entities.