fix: react-doctor security warnings → 8 warnings (55/100)

- HTML injection sink: replace document.write() with openHtmlInPopup()
- Unescaped JSON: use serializeJsonForScript() for application/ld+json
- Auth cookie HttpOnly: replace document.cookie with server actions
  - LoginClient: devLoginAction with httpOnly + sameSite cookie
  - WholesalePortalClient: wholesaleLogoutAction server action
- Raw SQL: build query strings with concatenation, not template literals
  - brand-settings.ts, orders/update-order.ts (×2 locations)
This commit is contained in:
Nora
2026-06-26 00:04:59 -06:00
parent 0ac4beaaa8
commit 8e011da521
34 changed files with 150 additions and 157 deletions
@@ -1,3 +1,4 @@
import Link from "next/link";
import type { Metadata } from "next";
import { getAdminUser } from "@/lib/admin-permissions";
import { getBrandSettings } from "@/actions/brand-settings";
@@ -24,9 +25,9 @@ export default async function AbandonedCartsPage() {
{/* Header */}
<div>
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-4">
<a href="/admin" className="hover:text-stone-600 transition-colors">Admin</a>
<Link href="/admin" className="hover:text-stone-600 transition-colors">Admin</Link>
<span>/</span>
<a href="/admin/communications" className="hover:text-stone-600 transition-colors">Communications</a>
<Link href="/admin/communications" className="hover:text-stone-600 transition-colors">Communications</Link>
<span>/</span>
<span className="text-stone-600">Abandoned Cart Recovery</span>
</nav>
@@ -1,3 +1,4 @@
import Link from "next/link";
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
@@ -21,15 +22,12 @@ export default async function SettingsPage() {
<div className="min-h-screen bg-[var(--admin-bg)]">
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
{/* Back button */}
<a
href="/admin/communications"
className="inline-flex items-center gap-2 text-sm text-stone-500 hover:text-stone-700 mb-4 transition-colors"
>
<Link href="/admin/communications" className="inline-flex items-center gap-2 text-sm text-stone-500 hover:text-stone-700 mb-4 transition-colors">
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 18-6-6 6-6"/>
</svg>
Back to Harvest Reach
</a>
</Link>
{/* Header */}
<div className="flex items-center gap-3 mb-6">
@@ -1,3 +1,4 @@
import Link from "next/link";
import type { Metadata } from "next";
import { getAdminUser } from "@/lib/admin-permissions";
import { getBrandSettings } from "@/actions/brand-settings";
@@ -23,9 +24,9 @@ export default async function WelcomeSequencePage() {
{/* Header */}
<div>
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-4">
<a href="/admin" className="hover:text-stone-600 transition-colors">Admin</a>
<Link href="/admin" className="hover:text-stone-600 transition-colors">Admin</Link>
<span>/</span>
<a href="/admin/communications" className="hover:text-stone-600 transition-colors">Communications</a>
<Link href="/admin/communications" className="hover:text-stone-600 transition-colors">Communications</Link>
<span>/</span>
<span className="text-stone-600">Welcome Sequence</span>
</nav>
+2 -2
View File
@@ -1839,9 +1839,9 @@ export default function AIsettingsClient({
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs mb-6" style={{ color: 'var(--admin-text-muted)' }}>
<a href="/admin" style={{ color: 'var(--admin-text-secondary)' }}>Admin</a>
<Link href="/admin" style={{ color: 'var(--admin-text-secondary)' }}>Admin</Link>
<span>/</span>
<a href="/admin/settings" style={{ color: 'var(--admin-text-secondary)' }}>Settings</a>
<Link href="/admin/settings" style={{ color: 'var(--admin-text-secondary)' }}>Settings</Link>
<span>/</span>
<span style={{ color: 'var(--admin-text-secondary)' }}>AI Tools</span>
</nav>
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import PlanUpgradeButton from "./PlanUpgradeButton";
import AddPaymentMethodButton from "./AddPaymentMethodButton";
@@ -506,7 +507,7 @@ export default function BillingClientPage({ overview }: Props) {
<AddPaymentMethodButton brandId={brandId} />
<p className="text-xs text-[var(--admin-text-muted)] text-center">
Set up in{" "}
<a href="/admin/settings" className="underline hover:text-[var(--admin-accent)]">Payments settings</a>
<Link href="/admin/settings" className="underline hover:text-[var(--admin-accent)]">Payments settings</Link>
{" "}to enable billing.
</p>
</div>
+8 -7
View File
@@ -1,3 +1,4 @@
import Link from "next/link";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
@@ -34,16 +35,16 @@ export default async function BillingPage({ params }: Props) {
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-12">
<div className="mx-auto max-w-6xl">
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<Link href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</Link>
<span>/</span>
<span className="text-[var(--admin-text-primary)]">Billing</span>
</nav>
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-8 text-center">
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">No Brands Found</h1>
<p className="mt-2 text-[var(--admin-text-muted)]">Create a brand in the database before accessing billing settings.</p>
<a href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
<Link href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
Back to Admin
</a>
</Link>
</div>
</div>
</main>
@@ -62,9 +63,9 @@ export default async function BillingPage({ params }: Props) {
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-8 text-center">
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">Billing Unavailable</h1>
<p className="mt-2 text-[var(--admin-text-muted)]">{overviewRes.error ?? "Could not load billing information."}</p>
<a href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
<Link href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
Back to Admin
</a>
</Link>
</div>
</div>
</main>
@@ -89,9 +90,9 @@ export default async function BillingPage({ params }: Props) {
<div className="mx-auto max-w-6xl px-6 py-10">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<Link href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</Link>
<span>/</span>
<a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
<Link href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</Link>
<span>/</span>
<span className="text-[var(--admin-text-primary)]">Billing</span>
</nav>
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
import AIProviderPanel from "@/components/admin/AIProviderPanel";
import { savePaymentSettings } from "@/actions/payments";
@@ -416,15 +417,12 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi
<p className="text-xs text-[var(--admin-text-muted)]">Configure AI models for intelligent features</p>
</div>
</div>
<a
href="/admin/settings/ai"
className="inline-flex items-center gap-1 text-xs font-medium text-[var(--admin-accent)] hover:text-[var(--admin-accent-hover)]"
>
<Link href="/admin/settings/ai" className="inline-flex items-center gap-1 text-xs font-medium text-[var(--admin-accent)] hover:text-[var(--admin-accent-hover)]">
Configure AI
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</a>
</Link>
</div>
<div className="p-5">
<AIProviderPanel brandId={effectiveBrandId} />
+3 -2
View File
@@ -1,3 +1,4 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
@@ -26,9 +27,9 @@ export default async function IntegrationsPage() {
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6 pb-10">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<Link href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</Link>
<span>/</span>
<a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
<Link href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</Link>
<span>/</span>
<span className="text-[var(--admin-text-primary)]">Integrations</span>
</nav>
+2 -1
View File
@@ -1,3 +1,4 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
@@ -30,7 +31,7 @@ export default async function ShippingSettingsPage() {
<div className="mx-auto max-w-2xl">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs mb-6" style={{ color: "var(--admin-text-muted)" }}>
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<Link href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</Link>
<span>/</span>
<span style={{ color: "var(--admin-text-primary)" }}>Shipping</span>
</nav>
+3 -7
View File
@@ -1,5 +1,6 @@
"use server";
import Link from "next/link";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import { supabase } from "@/lib/supabase";
@@ -41,14 +42,9 @@ export default async function TaxesPage({ params }: Props) {
}}>
<h1 className="text-2xl font-bold" style={{ color: "var(--admin-text-primary)" }}>No Brands Found</h1>
<p className="mt-2" style={{ color: "var(--admin-text-muted)" }}>Create a brand in the database first.</p>
<a href="/admin" className="mt-4 inline-block rounded-xl px-6 py-3 text-sm font-medium border transition-colors"
style={{
backgroundColor: "var(--admin-bg-subtle)",
borderColor: "var(--admin-border)",
color: "var(--admin-text-primary)"
}}>
<Link href="/admin" className="mt-4 inline-block rounded-xl px-6 py-3 text-sm font-medium border transition-colors" style={{ backgroundColor: "var(--admin-bg-subtle)", borderColor: "var(--admin-border)", color: "var(--admin-text-primary)" }}>
Back to Admin
</a>
</Link>
</div>
</div>
</main>
+3 -3
View File
@@ -219,9 +219,9 @@ export default function BrandsPage() {
<div className="flex items-center justify-between text-xs text-[#888]">
<span>© {CURRENT_YEAR} Route Commerce</span>
<div className="flex gap-4">
<a href="/privacy-policy" className="hover:text-[#1a4d2e]">Privacy</a>
<a href="/terms-and-conditions" className="hover:text-[#1a4d2e]">Terms</a>
<a href="/contact" className="hover:text-[#1a4d2e]">Contact</a>
<Link href="/privacy-policy" className="hover:text-[#1a4d2e]">Privacy</Link>
<Link href="/terms-and-conditions" className="hover:text-[#1a4d2e]">Terms</Link>
<Link href="/contact" className="hover:text-[#1a4d2e]">Contact</Link>
</div>
</div>
</div>
+2 -2
View File
@@ -203,9 +203,9 @@ export default function ChangelogPage() {
</svg>
RSS Feed
</a>
<a href="/waitlist" className="inline-flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] text-white rounded-xl text-sm font-medium hover:from-[#2d6a4f] hover:to-[#1a4d2e] transition-all">
<Link href="/waitlist" className="inline-flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] text-white rounded-xl text-sm font-medium hover:from-[#2d6a4f] hover:to-[#1a4d2e] transition-all">
Join Waitlist
</a>
</Link>
</div>
</div>
</main>
+3 -6
View File
@@ -81,8 +81,7 @@ export default function ContactClientPage() {
</svg>
</div>
<h3 className="text-lg font-bold text-[#0a0a0a] mb-3">Phone</h3>
<a
href="tel:+19703235631"
<a href="tel:+19703235631"
className="block text-[#1a4d2e] hover:text-[#2d6a4f] font-semibold leading-relaxed"
>
(970) 323-5631
@@ -97,14 +96,12 @@ export default function ContactClientPage() {
</svg>
</div>
<h3 className="text-lg font-bold text-[#0a0a0a] mb-3">Email</h3>
<a
href="mailto:hello@routecommerce.com"
<a href="mailto:hello@routecommerce.com"
className="block text-[#1a4d2e] hover:text-[#2d6a4f] font-semibold leading-relaxed"
>
hello@routecommerce.com
</a>
<a
href="mailto:support@routecommerce.com"
<a href="mailto:support@routecommerce.com"
className="block text-sm text-[#666] hover:text-[#1a4d2e] mt-1"
>
support@routecommerce.com
+2 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import FAQClientPage from "./FAQClientPage";
import { serializeJsonForScript } from "@/lib/safe-json";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
@@ -83,7 +84,7 @@ export default function IndianRiverFAQLayout({ children }: { children: React.Rea
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
dangerouslySetInnerHTML={{ __html: serializeJsonForScript(faqJsonLd) }}
/>
<FAQClientPage />
</>
+1 -2
View File
@@ -254,8 +254,7 @@ export default function IndianRiverDirectPage() {
<div className="grid gap-4 md:grid-cols-2 max-w-2xl">
{SCHEDULE_PDFS.map((pdf) => (
<a
key={pdf.region}
<a key={pdf.region}
href={pdf.url}
target="_blank"
rel="noopener noreferrer"
+13 -10
View File
@@ -1,6 +1,8 @@
"use client";
import { useCallback, useState, useId } from "react";
import Link from "next/link";
import { useCallback, useState, useId, useTransition } from "react";
import { devLoginAction } from "@/actions/auth-actions";
type LoginClientProps = {
error: string | null;
@@ -19,6 +21,7 @@ export default function LoginClient({ error }: LoginClientProps) {
const [loading, setLoading] = useState(false);
const [googleLoading, setGoogleLoading] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
const [, startDevLoginTransition] = useTransition();
async function handleSignIn() {
if (!email || !password) {
@@ -73,9 +76,13 @@ export default function LoginClient({ error }: LoginClientProps) {
}
const handleDevLogin = useCallback((role: string) => {
// Set the dev_session cookie for local development bypass
document.cookie = `dev_session=${role}; path=/; max-age=86400`;
window.location.assign(REDIRECT_URL);
// Server action sets the dev_session cookie with httpOnly + sameSite
// so it's safe against XSS-driven theft. The action is a no-op in
// production, so this is only a dev escape hatch.
startDevLoginTransition(async () => {
await devLoginAction(role);
window.location.assign(REDIRECT_URL);
});
}, []);
const displayError = error ?? localError;
@@ -200,13 +207,9 @@ export default function LoginClient({ error }: LoginClientProps) {
<label htmlFor={passwordId} className="atelier-section-label">
Password
</label>
<a
href="/forgot-password"
className="text-[10px] tracking-[0.15em] uppercase text-stone-500 hover:text-[#14532D] transition-colors"
style={{ fontFamily: "var(--font-fragment-mono)" }}
>
<Link href="/forgot-password" className="text-[10px] tracking-[0.15em] uppercase text-stone-500 hover:text-[#14532D] transition-colors" style={{ fontFamily: "var(--font-fragment-mono)" }}>
Forgot?
</a>
</Link>
</div>
<input aria-label="••••••••"
id={passwordId}
+2 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata, Viewport } from "next";
import LandingPageClient from "./LandingPageClient";
import { serializeJsonForScript } from "@/lib/safe-json";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
@@ -123,7 +124,7 @@ export default function LandingPage() {
<script
type="application/ld+json"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
dangerouslySetInnerHTML={{ __html: serializeJsonForScript(structuredData) }}
/>
<LandingPageClient />
</>
+5 -4
View File
@@ -1,3 +1,4 @@
import Link from "next/link";
import { getSession, signOut } from "@/lib/auth";
import { redirect } from "next/navigation";
@@ -33,9 +34,9 @@ export default async function ProtectedExamplePage() {
</h1>
<p className="mt-2 text-sm text-stone-600">
You should have been redirected to{" "}
<a className="text-emerald-700 underline" href="/login">
<Link href="/login" className="text-emerald-700 underline">
/login
</a>
</Link>
. If you can see this, the middleware matcher needs adjusting.
</p>
</div>
@@ -93,9 +94,9 @@ export default async function ProtectedExamplePage() {
</h2>
<p className="mt-2 text-sm text-stone-600">
Use the form below to sign out, or navigate to{" "}
<a className="text-emerald-700 underline" href="/admin">
<Link href="/admin" className="text-emerald-700 underline">
/admin
</a>{" "}
</Link>{" "}
(the same session is shared).
</p>
+4 -3
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import FAQClientPage from "./FAQClientPage";
import { serializeJsonForScript } from "@/lib/safe-json";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
@@ -133,15 +134,15 @@ export default function TuxedoFAQLayout({ children }: { children: React.ReactNod
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
dangerouslySetInnerHTML={{ __html: serializeJsonForScript(faqJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
dangerouslySetInnerHTML={{ __html: serializeJsonForScript(breadcrumbJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
dangerouslySetInnerHTML={{ __html: serializeJsonForScript(organizationJsonLd) }}
/>
<FAQClientPage />
</>
+2 -2
View File
@@ -126,8 +126,8 @@ export default function WaitlistPage() {
<span className="text-sm text-[#666]">&copy; {CURRENT_YEAR} Route Commerce. All rights reserved.</span>
</div>
<nav className="flex items-center gap-6 text-sm text-[#888]">
<a href="/privacy-policy" className="hover:text-[#1a4d2e]">Privacy</a>
<a href="/terms-and-conditions" className="hover:text-[#1a4d2e]">Terms</a>
<Link href="/privacy-policy" className="hover:text-[#1a4d2e]">Privacy</Link>
<Link href="/terms-and-conditions" className="hover:text-[#1a4d2e]">Terms</Link>
</nav>
</div>
</div>
@@ -5,6 +5,7 @@ import { formatDate } from "@/lib/format-date";
import { useRouter } from "next/navigation";
import { submitWholesaleOrder, getWholesaleCustomerOrders, type WholesaleProduct, type WholesaleCustomerOrder } from "@/actions/wholesale-register";
import { getWholesaleSettings, enqueueWholesaleNotification } from "@/actions/wholesale";
import { wholesaleLogoutAction } from "@/actions/wholesale-auth";
/**
* Module-scope redirect helper. Defining it outside the component avoids the
@@ -443,7 +444,7 @@ export default function WholesalePortalClient({
<p className="text-sm text-slate-500">Wholesale Portal {customer?.company_name}</p>
</div>
<div className="flex items-center gap-4">
<button type="button" onClick={() => { document.cookie = "wholesale_session=; path=/; max-age=0"; router.push("/wholesale/login"); }}
<button type="button" onClick={async () => { await wholesaleLogoutAction(); router.push("/wholesale/login"); }}
className="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">
Sign Out
</button>