Skip to content

Instantly share code, notes, and snippets.

@angusgrant
Created November 5, 2022 15:32
Show Gist options
  • Save angusgrant/b80bca1273c1faa1bf2790f661f9d38b to your computer and use it in GitHub Desktop.
Save angusgrant/b80bca1273c1faa1bf2790f661f9d38b to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Dragon Trainer Monthly - XSS</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body {
margin: 0 auto;
max-width: 40em;
width: 88%;
}
article {
margin-bottom: 3em;
}
</style>
</head>
<body>
<h1>Dragon Trainer Monthly - XSS</h1>
<div id="app"></div>
<script>
// Get the app element
let app = document.querySelector('#app');
/**
* Find the first matching author
* @param {String} name The author name
* @param {Array} authors The author details
* @return {Array} The author
*/
function getAuthor (name, authors) {
return authors.find(function (author) {
return author.author === name;
});
}
/**
* Render an error message if fetch fails
*/
function renderFail () {
app.innerHTML = '<p>The dragons burned all the copies. Unable to get new articles at this time. Sorry!</p>';
}
/**
* Render articles into the DOM
* @param {Array} articles The articles to render
* @param {Array} authors The author details
*/
function render (articles, authors) {
// If there are no articles to show
if (!articles || articles.length < 1) {
renderFail();
return;
}
// Create a new array of markup strings with array.map(), then
// Combine them into one string with array.join(), then
// Insert them into the DOM with innerHTML
app.innerHTML = articles.map(function (article) {
let author = getAuthor(article.author, authors);
return cleanHTML(`
<article>
<h2><a href="${article.url}">${article.title}</a></h2>
<p><em>By ${author ? `${author.author} - ${author.bio}` : article.author}</em></p>
<p>${article.article}</p>
</article>`);
}).join('');
}
// Get articles
Promise.all([
fetch('https://vanillajsacademy.com/api/dragons.json'),
fetch('https://vanillajsacademy.com/api/dragons-authors.json'),
]).then(function (responses) {
return Promise.all(responses.map(function (response) {
return response.json();
}));
}).then(function (data) {
// Render them into the DOM
render(data[0].articles, data[1].authors);
}).catch(function (error) {
console.warn(error);
renderFail();
});
/*!
* Sanitize an HTML string
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {String} str The HTML string to sanitize
* @param {Boolean} nodes If true, returns HTML nodes instead of a string
* @return {String|NodeList} The sanitized string or nodes
*/
function cleanHTML (str, nodes) {
/**
* Convert the string to an HTML document
* @return {Node} An HTML document
*/
function stringToHTML () {
let parser = new DOMParser();
let doc = parser.parseFromString(str, 'text/html');
return doc.body || document.createElement('body');
}
/**
* Remove <script> elements
* @param {Node} html The HTML
*/
function removeScripts (html) {
let scripts = html.querySelectorAll('script');
for (let script of scripts) {
script.remove();
}
}
/**
* Check if the attribute is potentially dangerous
* @param {String} name The attribute name
* @param {String} value The attribute value
* @return {Boolean} If true, the attribute is potentially dangerous
*/
function isPossiblyDangerous (name, value) {
let val = value.replace(/\s+/g, '').toLowerCase();
if (['src', 'href', 'xlink:href'].includes(name)) {
if (val.includes('javascript:') || val.includes('data:text/html')) return true;
}
if (name.startsWith('on')) return true;
}
/**
* Remove potentially dangerous attributes from an element
* @param {Node} elem The element
*/
function removeAttributes (elem) {
// Loop through each attribute
// If it's dangerous, remove it
let atts = elem.attributes;
for (let {name, value} of atts) {
if (!isPossiblyDangerous(name, value)) continue;
elem.removeAttribute(name);
}
}
/**
* Remove dangerous stuff from the HTML document's nodes
* @param {Node} html The HTML document
*/
function clean (html) {
let nodes = html.children;
for (let node of nodes) {
removeAttributes(node);
clean(node);
}
}
// Convert the string to HTML
let html = stringToHTML();
// Sanitize it
removeScripts(html);
clean(html);
// If the user wants HTML nodes back, return them
// Otherwise, pass a sanitized string back
return nodes ? html.childNodes : html.innerHTML;
}</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment