Use html template tags to escape user input

Merged safe html changes from Steven.
This commit is contained in:
Steven 2022-12-19 22:47:47 -08:00 committed by Thomas Mathews
parent 7954c75841
commit 7a3a4077e8
4 changed files with 77 additions and 38 deletions

View file

@ -8,6 +8,7 @@
<link rel="stylesheet" href="css/utils.css?v=1"> <link rel="stylesheet" href="css/utils.css?v=1">
<link rel="stylesheet" href="css/styles.css?v=13"> <link rel="stylesheet" href="css/styles.css?v=13">
<link rel="stylesheet" href="css/responsive.css?v=10"> <link rel="stylesheet" href="css/responsive.css?v=10">
<script defer src="js/ui/safe-html.js?v=1"></script>
<script defer src="js/util.js?v=5"></script> <script defer src="js/util.js?v=5"></script>
<script defer src="js/ui/util.js?v=8"></script> <script defer src="js/ui/util.js?v=8"></script>
<script defer src="js/ui/render.js?v=15"></script> <script defer src="js/ui/render.js?v=15"></script>

View file

@ -7,21 +7,21 @@ function linkify(text="", show_media=false) {
} catch (err) { } catch (err) {
return match; return match;
} }
let html; let markup;
if (show_media && is_img_url(parsed.pathname)) { if (show_media && is_img_url(parsed.pathname)) {
html = ` markup = html`
<img class="inline-img clickable" src="${url}" onclick="open_media_preview('${url}', 'image')"/> <img class="inline-img clickable" src="${url}" onclick="open_media_preview('${url}', 'image')"/>
`; `;
} else if (show_media && is_video_url(parsed.pathname)) { } else if (show_media && is_video_url(parsed.pathname)) {
html = ` markup = html`
<video controls class="inline-img" /> <video controls class="inline-img" />
<source src="${url}"> <source src="${url}">
</video> </video>
`; `;
} else { } else {
html = `<a target="_blank" rel="noopener noreferrer" href="${url}">${url}</a>`; markup = html`<a target="_blank" rel="noopener noreferrer" href="${url}">${url}</a>`;
} }
return p1+html; return p1+markup;
}) })
} }

View file

@ -67,13 +67,13 @@ function render_replying_to(model, ev) {
if (pubkeys.length === 0 && ev.refs.reply) { if (pubkeys.length === 0 && ev.refs.reply) {
const replying_to = model.all_events[ev.refs.reply] const replying_to = model.all_events[ev.refs.reply]
if (!replying_to) if (!replying_to)
return `<div class="replying-to small-txt">reply to ${ev.refs.reply}</div>`; return html`<div class="replying-to small-txt">reply to ${ev.refs.reply}</div>`;
pubkeys = [replying_to.pubkey] pubkeys = [replying_to.pubkey]
} }
const names = pubkeys.map((pk) => { const names = pubkeys.map((pk) => {
return render_mentioned_name(pk, model.profiles[pk]); return render_mentioned_name(pk, model.profiles[pk]);
}).join(", ") }).join(", ")
return ` return html`
<span class="replying-to small-txt"> <span class="replying-to small-txt">
replying to ${names} replying to ${names}
</span> </span>
@ -100,23 +100,23 @@ function render_comment_body(model, ev, opts) {
// Only show media for content that is by friends. // Only show media for content that is by friends.
const show_media = !opts.is_composing && const show_media = !opts.is_composing &&
model.contacts.friends.has(ev.pubkey); model.contacts.friends.has(ev.pubkey);
return ` return html`
<div> <div>
${render_replying_to(model, ev)} $${render_replying_to(model, ev)}
${render_shared_by(ev, opts)} $${render_shared_by(ev, opts)}
</div> </div>
<p> <p>
${format_content(ev, show_media)} $${format_content(ev, show_media)}
</p> </p>
${render_reactions(model, ev)} $${render_reactions(model, ev)}
${bar}` $${bar}`
} }
function render_shared_by(ev, opts) { function render_shared_by(ev, opts) {
if (!opts.shared) if (!opts.shared)
return ""; return "";
const { profile, pubkey } = opts.shared const { profile, pubkey } = opts.shared
return `<div class="shared-by">Shared by ${render_name(pubkey, profile)} return html`<div class="shared-by">Shared by $${render_name(pubkey, profile)}
</div>` </div>`
} }
@ -137,22 +137,22 @@ function render_event(model, ev, opts={}) {
const border_bottom = opts.is_composing || has_bot_line ? "" : "bottom-border"; const border_bottom = opts.is_composing || has_bot_line ? "" : "bottom-border";
let thread_btn = ""; let thread_btn = "";
if (!reply_line_bot) reply_line_bot = ''; if (!reply_line_bot) reply_line_bot = '';
return `<div id="ev${ev.id}" class="event ${border_bottom}"> return html`<div id="ev${ev.id}" class="event ${border_bottom}">
<div class="userpic"> <div class="userpic">
${render_reply_line_top(has_top_line)} $${render_reply_line_top(has_top_line)}
${render_pfp(ev.pubkey, profile)} $${render_pfp(ev.pubkey, profile)}
${reply_line_bot} $${reply_line_bot}
</div> </div>
<div class="event-content"> <div class="event-content">
<div class="info"> <div class="info">
${render_name(ev.pubkey, profile)} $${render_name(ev.pubkey, profile)}
<span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span> <span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span>
<button class="icon" title="View Thread" role="view-event" onclick="open_thread('${thread_root}')"> <button class="icon" title="View Thread" role="view-event" onclick="open_thread('${thread_root}')">
<img class="icon svg small" src="icon/open-thread.svg"/> <img class="icon svg small" src="icon/open-thread.svg"/>
</button> </button>
</div> </div>
<div class="comment"> <div class="comment">
${render_comment_body(model, ev, opts)} $${render_comment_body(model, ev, opts)}
</div> </div>
</div> </div>
</div>` </div>`
@ -161,17 +161,17 @@ function render_event(model, ev, opts={}) {
function render_event_nointeract(model, ev, opts={}) { function render_event_nointeract(model, ev, opts={}) {
const profile = model.profiles[ev.pubkey]; const profile = model.profiles[ev.pubkey];
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000) const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
return `<div class="event border-bottom"> return html`<div class="event border-bottom">
<div class="userpic"> <div class="userpic">
${render_pfp(ev.pubkey, profile)} $${render_pfp(ev.pubkey, profile)}
</div> </div>
<div class="event-content"> <div class="event-content">
<div class="info"> <div class="info">
${render_name(ev.pubkey, profile)} $${render_name(ev.pubkey, profile)}
<span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span> <span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span>
</div> </div>
<div class="comment"> <div class="comment">
${render_comment_body(model, ev, opts)} $${render_comment_body(model, ev, opts)}
</div> </div>
</div> </div>
</div>` </div>`
@ -180,9 +180,9 @@ function render_event_nointeract(model, ev, opts={}) {
function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) { function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) {
const reaction = reactions[our_pubkey] const reaction = reactions[our_pubkey]
if (!reaction) { if (!reaction) {
return `onclick="send_reply('${emoji}', '${reacting_to}')"` return html`onclick="send_reply('${emoji}', '${reacting_to}')"`
} else { } else {
return `onclick="delete_post('${reaction.id}')"` return html`onclick="delete_post('${reaction.id}')"`
} }
} }
@ -200,12 +200,12 @@ function render_reaction_group(model, emoji, reactions, reacting_to) {
} }
let onclick = render_react_onclick(model.pubkey, let onclick = render_react_onclick(model.pubkey,
reacting_to.id, emoji, reactions); reacting_to.id, emoji, reactions);
return ` return html`
<span ${onclick} class="reaction-group clickable"> <span $${onclick} class="reaction-group clickable">
<span class="reaction-emoji"> <span class="reaction-emoji">
${emoji} ${emoji}
</span> </span>
${str} $${str}
</span>`; </span>`;
} }
@ -214,7 +214,7 @@ function render_action_bar(model, ev, opts={}) {
let { can_delete } = opts; let { can_delete } = opts;
let delete_html = "" let delete_html = ""
if (can_delete) { if (can_delete) {
delete_html = ` delete_html = html`
<button class="icon" title="Delete" onclick="delete_post_confirm('${ev.id}')"> <button class="icon" title="Delete" onclick="delete_post_confirm('${ev.id}')">
<img class="icon svg small" src="icon/event-delete.svg"/> <img class="icon svg small" src="icon/event-delete.svg"/>
</button>` </button>`
@ -224,7 +224,7 @@ function render_action_bar(model, ev, opts={}) {
const reaction = model_get_reacts_to(model, pubkey, ev.id, R_HEART); const reaction = model_get_reacts_to(model, pubkey, ev.id, R_HEART);
const liked = !!reaction; const liked = !!reaction;
const reaction_id = reaction ? reaction.id : ""; const reaction_id = reaction ? reaction.id : "";
return ` return html`
<div class="action-bar"> <div class="action-bar">
<button class="icon" title="Reply" onclick="reply_to('${ev.id}')"> <button class="icon" title="Reply" onclick="reply_to('${ev.id}')">
<img class="icon svg small" src="icon/event-reply.svg"/> <img class="icon svg small" src="icon/event-reply.svg"/>
@ -233,12 +233,12 @@ function render_action_bar(model, ev, opts={}) {
onclick="click_toggle_like(this)" onclick="click_toggle_like(this)"
data-reaction-id="${reaction_id}" data-reaction-id="${reaction_id}"
data-reacting-to="${ev.id}" data-reacting-to="${ev.id}"
title="${ab(liked, 'Unlike', 'Like')}"> title="$${ab(liked, 'Unlike', 'Like')}">
<img class="icon svg small ${ab(liked, 'dark-noinvert', '')}" <img class="icon svg small ${ab(liked, 'dark-noinvert', '')}"
src="${ab(liked, IMG_EVENT_LIKED, IMG_EVENT_LIKE)}"/> src="$${ab(liked, IMG_EVENT_LIKED, IMG_EVENT_LIKE)}"/>
</button> </button>
<!--<button class="icon" title="Share" onclick=""><i class="fa fa-fw fa-link"></i></a>--> <!--<button class="icon" title="Share" onclick=""><i class="fa fa-fw fa-link"></i></a>-->
${delete_html} $${delete_html}
<!--<button class="icon" title="View raw Nostr event." onclick=""><i class="fa-solid fa-fw fa-code"></i></a>--> <!--<button class="icon" title="View raw Nostr event." onclick=""><i class="fa-solid fa-fw fa-code"></i></a>-->
</div> </div>
` `
@ -254,7 +254,7 @@ function render_reactions_inner(model, ev) {
} }
function render_reactions(model, ev) { function render_reactions(model, ev) {
return `<div class="reactions">${render_reactions_inner(model, ev)}</div>` return html`<div class="reactions">${render_reactions_inner(model, ev)}</div>`
} }
// Utility Methods // Utility Methods
@ -274,7 +274,7 @@ function render_mentioned_name(pk, profile) {
function render_name(pk, profile, prefix="") { function render_name(pk, profile, prefix="") {
// Beware of whitespace. // Beware of whitespace.
return `<span>${prefix}<span class="username clickable" data-pubkey="${pk}" return html`<span>${prefix}<span class="username clickable" data-pubkey="${pk}"
onclick="open_profile('${pk}')" onclick="open_profile('${pk}')"
>${fmt_profile_name(profile, fmt_pubkey(pk))}</span></span>` >${fmt_profile_name(profile, fmt_pubkey(pk))}</span></span>`
} }
@ -284,7 +284,7 @@ function render_pfp(pk, profile, opts={}) {
let str = `class="pfp clickable" onclick="open_profile('${pk}')"`; let str = `class="pfp clickable" onclick="open_profile('${pk}')"`;
if (opts.noclick) if (opts.noclick)
str = "class='pfp'"; str = "class='pfp'";
return `<img return html`<img
${str} ${str}
data-pubkey="${pk}" data-pubkey="${pk}"
title="${name}" title="${name}"
@ -293,7 +293,7 @@ function render_pfp(pk, profile, opts={}) {
} }
function render_loading_spinner() { function render_loading_spinner() {
return ` return html`
<div class="loading-events"> <div class="loading-events">
<div class="loader" title="Loading..."> <div class="loader" title="Loading...">
<img class="dark-invert" src="icon/loader-fragment.svg"/> <img class="dark-invert" src="icon/loader-fragment.svg"/>

38
web/js/ui/safe-html.js Normal file
View file

@ -0,0 +1,38 @@
// https://github.com/AntonioVdlC/html-template-tag
const chars = {
"&": "&amp;",
">": "&gt;",
"<": "&lt;",
'"': "&quot;",
"'": "&#39;",
"`": "&#96;",
};
// Dynamically create a RegExp from the `chars` object
const re = new RegExp(Object.keys(chars).join("|"), "g");
// Return the escaped string
function escape(str) {
return String(str).replace(re, (match) => chars[match]);
}
function html(
literals,
...substs
) {
return literals.raw.reduce((acc, lit, i) => {
let subst = substs[i - 1];
if (Array.isArray(subst)) {
subst = subst.join("");
} else if (literals.raw[i - 1] && literals.raw[i - 1].endsWith("$")) {
// If the interpolation is preceded by a dollar sign,
// substitution is considered safe and will not be escaped
acc = acc.slice(0, -1);
} else {
subst = escape(subst);
}
return acc + subst + lit;
});
}