Fix 8 bugs: XSS, race condition, null safety, form reset, pricing

1. XSS: error messages use textContent by default, innerHTML only
   for controlled HTML (CORES link) via allowHtml flag
2. XSS: name search errors built with DOM API, not innerHTML
3. Race condition: concurrent FRN lookups cancel prior request
   via AbortController tracking
4. Null safety: DOM element guards with error logging
5. Null safety: check.detail uses || "" fallback, \n → <br>
6. Quote form: auto-resets after 3 seconds on successful submit
7. Pricing: discount uses Math.round(total*15)/100 for cent precision
8. Future-proofing: parseFloat for prices instead of parseInt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-04-27 22:34:08 -05:00
parent 4853f67f5e
commit 6171c64b90

View file

@ -202,9 +202,17 @@ import Base from "../../layouts/Base.astro";
const errorMessage = document.getElementById("error-message"); const errorMessage = document.getElementById("error-message");
const resultsEl = document.getElementById("results"); const resultsEl = document.getElementById("results");
// Guard against missing DOM elements
if (!nameInput || !frnInput || !loadingEl || !errorBox || !errorMessage || !resultsEl) {
console.error("[fcc-check] Required DOM elements missing");
}
// Race condition guard — cancel prior fetch when new one starts
let currentController = null;
// --- Name search --- // --- Name search ---
nameSearchBtn.addEventListener("click", runNameSearch); nameSearchBtn?.addEventListener("click", runNameSearch);
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); }); nameInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
async function runNameSearch() { async function runNameSearch() {
const name = nameInput.value.trim(); const name = nameInput.value.trim();
@ -258,7 +266,11 @@ import Base from "../../layouts/Base.astro";
}); });
}); });
} catch (err) { } catch (err) {
nameResults.innerHTML = `<p class="text-sm text-red-600">Search error: ${err.message}</p>`; const errP = document.createElement("p");
errP.className = "text-sm text-red-600";
errP.textContent = "Search error: " + (err.message || "Unknown error");
nameResults.innerHTML = "";
nameResults.appendChild(errP);
} }
} }
@ -321,6 +333,15 @@ import Base from "../../layouts/Base.astro";
statusEl.textContent = "✓ Request submitted! We'll email you within 1 business day."; statusEl.textContent = "✓ Request submitted! We'll email you within 1 business day.";
statusEl.className = "mt-2 text-xs text-green-700"; statusEl.className = "mt-2 text-xs text-green-700";
btn.textContent = "Sent ✓"; btn.textContent = "Sent ✓";
// Reset form after 3 seconds
setTimeout(() => {
document.getElementById("quote-form")?.classList.add("hidden");
document.getElementById("quote-name").value = "";
document.getElementById("quote-email").value = "";
btn.textContent = "Request Assessment";
btn.disabled = false;
statusEl.classList.add("hidden");
}, 3000);
} else { } else {
throw new Error("Failed"); throw new Error("Failed");
} }
@ -360,13 +381,20 @@ import Base from "../../layouts/Base.astro";
errorBox.classList.add("hidden"); errorBox.classList.add("hidden");
loadingEl.classList.remove("hidden"); loadingEl.classList.remove("hidden");
try { // Cancel any prior in-flight request
if (currentController) currentController.abort();
const controller = new AbortController(); const controller = new AbortController();
currentController = controller;
try {
const timeout = setTimeout(() => controller.abort(), 30000); const timeout = setTimeout(() => controller.abort(), 30000);
const res = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`, { signal: controller.signal }); const res = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`, { signal: controller.signal });
clearTimeout(timeout); clearTimeout(timeout);
// Ignore if a newer request superseded this one
if (currentController !== controller) return;
if (!res.ok) { if (!res.ok) {
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
if (res.status === 400) { if (res.status === 400) {
@ -376,7 +404,7 @@ import Base from "../../layouts/Base.astro";
} }
const data = await res.json(); const data = await res.json();
if (!data.entity_name && !data.cores?.entity_name && !data.filer_499) { if (!data.entity_name && !data.cores?.entity_name && !data.filer_499) {
showError('FRN ' + frn + ' was not found in any FCC database. Verify your number at <a href="https://apps.fcc.gov/coresWeb/publicHome.do" target="_blank" style="color:#1e40af;text-decoration:underline;">FCC CORES</a>.'); showError('FRN ' + frn + ' was not found in any FCC database. Verify your number at <a href="https://apps.fcc.gov/coresWeb/publicHome.do" target="_blank" style="color:#1e40af;text-decoration:underline;">FCC CORES</a>.', true);
return; return;
} }
renderResults(data); renderResults(data);
@ -391,8 +419,12 @@ import Base from "../../layouts/Base.astro";
} }
} }
function showError(msg) { function showError(msg, allowHtml) {
if (allowHtml) {
errorMessage.innerHTML = msg; errorMessage.innerHTML = msg;
} else {
errorMessage.textContent = msg;
}
errorBox.classList.remove("hidden"); errorBox.classList.remove("hidden");
} }
@ -684,7 +716,7 @@ import Base from "../../layouts/Base.astro";
function attachF499Handlers() { function attachF499Handlers() {
card.querySelector(".f499-yes")?.addEventListener("click", () => { card.querySelector(".f499-yes")?.addEventListener("click", () => {
showF499Result( showF499Result(
`${check.detail}\n${eName} had telecom revenue — filing is required for each missed year.`, `${check.detail || ""}<br>${eName} had telecom revenue — filing is required for each missed year.`,
"red" "red"
); );
}); });
@ -808,7 +840,7 @@ import Base from "../../layouts/Base.astro";
boxes.forEach((cb) => { boxes.forEach((cb) => {
if (cb.checked) { if (cb.checked) {
const p = parseInt(cb.dataset.price, 10); const p = parseFloat(cb.dataset.price) || 0;
selectedIds.push(cb.dataset.id); selectedIds.push(cb.dataset.id);
if (p > 0) { if (p > 0) {
// Exclude dc_agent from discount calc // Exclude dc_agent from discount calc
@ -825,7 +857,7 @@ import Base from "../../layouts/Base.astro";
const fmt = (v) => v.toFixed(2); const fmt = (v) => v.toFixed(2);
if (pricedCount >= 2) { if (pricedCount >= 2) {
const discount = Math.round(total * 0.15); const discount = Math.round(total * 15) / 100; // Precise to cents
const final_ = total - discount; const final_ = total - discount;
discountHtml = `<div class="flex justify-between text-sm"> discountHtml = `<div class="flex justify-between text-sm">
<span class="text-gray-600">Subtotal</span> <span class="text-gray-600">Subtotal</span>