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 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 ---
nameSearchBtn.addEventListener("click", runNameSearch);
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
nameSearchBtn?.addEventListener("click", runNameSearch);
nameInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
async function runNameSearch() {
const name = nameInput.value.trim();
@ -258,7 +266,11 @@ import Base from "../../layouts/Base.astro";
});
});
} 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.className = "mt-2 text-xs text-green-700";
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 {
throw new Error("Failed");
}
@ -360,13 +381,20 @@ import Base from "../../layouts/Base.astro";
errorBox.classList.add("hidden");
loadingEl.classList.remove("hidden");
// Cancel any prior in-flight request
if (currentController) currentController.abort();
const controller = new AbortController();
currentController = controller;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
const res = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`, { signal: controller.signal });
clearTimeout(timeout);
// Ignore if a newer request superseded this one
if (currentController !== controller) return;
if (!res.ok) {
const body = await res.json().catch(() => ({}));
if (res.status === 400) {
@ -376,7 +404,7 @@ import Base from "../../layouts/Base.astro";
}
const data = await res.json();
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;
}
renderResults(data);
@ -391,8 +419,12 @@ import Base from "../../layouts/Base.astro";
}
}
function showError(msg) {
errorMessage.innerHTML = msg;
function showError(msg, allowHtml) {
if (allowHtml) {
errorMessage.innerHTML = msg;
} else {
errorMessage.textContent = msg;
}
errorBox.classList.remove("hidden");
}
@ -684,7 +716,7 @@ import Base from "../../layouts/Base.astro";
function attachF499Handlers() {
card.querySelector(".f499-yes")?.addEventListener("click", () => {
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"
);
});
@ -808,7 +840,7 @@ import Base from "../../layouts/Base.astro";
boxes.forEach((cb) => {
if (cb.checked) {
const p = parseInt(cb.dataset.price, 10);
const p = parseFloat(cb.dataset.price) || 0;
selectedIds.push(cb.dataset.id);
if (p > 0) {
// Exclude dc_agent from discount calc
@ -825,7 +857,7 @@ import Base from "../../layouts/Base.astro";
const fmt = (v) => v.toFixed(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;
discountHtml = `<div class="flex justify-between text-sm">
<span class="text-gray-600">Subtotal</span>