Created
November 5, 2022 15:32
-
-
Save angusgrant/b80bca1273c1faa1bf2790f661f9d38b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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