From 87f98d914da4d2204d31e3c1388cd7fea32db927 Mon Sep 17 00:00:00 2001 From: Kharec Date: Mon, 2 Mar 2026 07:35:23 +0100 Subject: [PATCH] fix: improve JS security and error handling in html.gleam --- src/html.gleam | 119 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 38 deletions(-) diff --git a/src/html.gleam b/src/html.gleam index 45cae9b..581f7c1 100644 --- a/src/html.gleam +++ b/src/html.gleam @@ -121,6 +121,11 @@ fn decrypt_js(encrypted_content: String) -> String { content: document.getElementById('content-display') }; + if (!els.loading || !els.error || !els.content) { + console.error('Required DOM elements not found'); + return; + } + if (!hash) { els.loading.classList.add('hidden'); els.error.classList.remove('hidden'); @@ -128,7 +133,9 @@ fn decrypt_js(encrypted_content: String) -> String { } try { - const keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/'); + let keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/'); + while (keyBase64.length % 4) keyBase64 += '='; + const encryptedBytes = Uint8Array.from(atob(encryptedContent), c => c.charCodeAt(0)); const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0)); const iv = encryptedBytes.slice(0, 12); @@ -150,70 +157,106 @@ fn decrypt_js(encrypted_content: String) -> String { } fn escape_js_string(s: String) -> String { - s - |> string.replace("\\", "\\\\") - |> string.replace("'", "\\'") - |> string.replace("\"", "\\\"") - |> string.replace("\n", "\\n") - |> string.replace("\r", "\\r") + let escaped = + s + |> string.replace("\\", "\\\\") + |> string.replace("'", "\\'") + |> string.replace("\"", "\\\"") + |> string.replace("\n", "\\n") + |> string.replace("\r", "\\r") + |> string.replace("<", "\\u003c") + |> string.replace(">", "\\u003e") + |> string.replace("&", "\\u0026") + + escaped } fn crypto_js() -> String { " async function encryptContent(content) { - const encoder = new TextEncoder(); - const data = encoder.encode(content); - const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data); - const keyData = await crypto.subtle.exportKey('raw', key); - return { - encrypted: btoa(String.fromCharCode(...new Uint8Array([...iv, ...new Uint8Array(encrypted)]))), - key: btoa(String.fromCharCode(...new Uint8Array(keyData))) - }; + try { + const encoder = new TextEncoder(); + const data = encoder.encode(content); + const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data); + const keyData = await crypto.subtle.exportKey('raw', key); + return { + encrypted: btoa(String.fromCharCode(...iv, ...new Uint8Array(encrypted))), + key: btoa(String.fromCharCode(...new Uint8Array(keyData))) + }; + } catch (e) { + console.error('Encryption failed:', e); + throw new Error('Failed to encrypt content'); + } } function base64ToUrlSafe(base64) { - return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, ''); + return base64.split('+').join('-').split('/').join('_').replace(/=+$/g, ''); } function showShareUrl(url) { const card = document.querySelector('.card'); + if (!card) { + console.error('Card element not found'); + return; + } const div = document.createElement('div'); div.className = 'share-url'; - div.innerHTML = ''; + const label = document.createElement('label'); + label.textContent = 'Share this URL'; + div.appendChild(label); const input = document.createElement('input'); input.type = 'text'; input.readOnly = true; input.value = url; - input.onclick = () => input.select(); + input.addEventListener('click', () => input.select()); div.appendChild(input); card.insertBefore(div, card.firstChild); input.select(); } -document.getElementById('paste-form').addEventListener('submit', async (e) => { +document.getElementById('paste-form')?.addEventListener('submit', async (e) => { e.preventDefault(); - const content = document.getElementById('content').value; - const { encrypted, key } = await encryptContent(content); - document.getElementById('encrypted-content').value = encrypted; - const csrfToken = document.getElementById('csrf-token').value; + + const submitButton = e.target.querySelector('button[type=\"submit\"]'); + const contentInput = document.getElementById('content'); + const encryptedInput = document.getElementById('encrypted-content'); + const csrfInput = document.getElementById('csrf-token'); + + if (!contentInput || !encryptedInput || !csrfInput) { + console.error('Required form elements not found'); + return; + } + + if (submitButton) submitButton.disabled = true; + + try { + const content = contentInput.value; + const { encrypted, key } = await encryptContent(content); + encryptedInput.value = encrypted; + const csrfToken = csrfInput.value; - const res = await fetch('/', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}` - }); + const res = await fetch('/', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}` + }); - const html = await res.text(); - const doc = new DOMParser().parseFromString(html, 'text/html'); - const pasteId = doc.querySelector('[data-paste-id]')?.getAttribute('data-paste-id'); - if (!pasteId) return; + const html = await res.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + const pasteId = doc.querySelector('[data-paste-id]')?.getAttribute('data-paste-id'); + if (!pasteId) throw new Error('No paste ID returned'); - const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`; - document.body.replaceChildren(...doc.body.childNodes); - showShareUrl(pasteUrl); - history.replaceState({}, '', pasteUrl); + const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`; + document.body.replaceChildren(...doc.body.childNodes); + showShareUrl(pasteUrl); + history.replaceState({}, '', pasteUrl); + } catch (err) { + console.error('Form submission failed:', err); + alert('Failed to create paste. Please try again.'); + if (submitButton) submitButton.disabled = false; + } }); " }