Fix remaining security vulnerabilities
- H1: Add missing sanitize() to help center content rendering - H2: Tighten CSP by removing unsafe-inline/unsafe-eval from script-src; move theme init to external JS file with meta tag for defaultDark - M1: Add SSRF protection for radio API URLs (block private IPs) - M2: Add rate limiting to SSO ticket endpoint (5 req/30s per user) - M4: Document locale validation safety in i18n dynamic import - L1: Truncate stacktraces in admin commandocentrum to first 20 lines
This commit is contained in:
1 parent
5628e7d6b7
commit
10523e58ce
9 files changed
+52
-29
No files matched your search
@@ -423,8 +423,13 @@ export default async function CommandoCentrum() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{errors.map((e) => {
|
||||
{errors.map((e) => {
|
||||
const trace = decodeStacktrace(e.stacktrace);
|
||||
// Truncate stacktraces to first 20 lines to avoid info disclosure.
|
||||
const snippet = trace
|
||||
? trace.split("\n").slice(0, 20).join("\n") +
|
||||
(trace.split("\n").length > 20 ? "\n… (truncated)" : "")
|
||||
: "(empty)";
|
||||
return (
|
||||
<tr key={e.id}>
|
||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
@@ -436,7 +441,7 @@ export default async function CommandoCentrum() {
|
||||
</td>
|
||||
<td>
|
||||
<pre className="text-xs p-2 bg-gray-50 dark:bg-black/20 rounded overflow-auto max-h-[160px]">
|
||||
{trace || "(empty)"}
|
||||
{snippet}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { issueSsoTicket } from "@/lib/auth/sso-ticket";
|
||||
|
||||
@@ -13,14 +14,18 @@ export async function GET() {
|
||||
}
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
// Throttle SSO ticket generation (5 per 30s per user) — prevent ticket spam.
|
||||
if (!rateLimit(`sso:${userId}`, 5, 30_000).ok) {
|
||||
return new Response(JSON.stringify({ error: "Rate limited" }), { status: 429 });
|
||||
}
|
||||
|
||||
const [hotelName, clientUrl] = await Promise.all([
|
||||
siteSettings.get("hotel_name", "Atom"),
|
||||
siteSettings.get("nitro_client_url", ""),
|
||||
]);
|
||||
|
||||
const hdrs = await headers();
|
||||
const ip =
|
||||
hdrs.get("x-forwarded-for")?.split(",")[0]?.trim() ?? hdrs.get("x-real-ip") ?? "0.0.0.0";
|
||||
const ip = await clientIp();
|
||||
|
||||
const ticket = await issueSsoTicket(prisma, userId, hotelName ?? "Atom", ip);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ContentCard } from "@/components/public/ui";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { sanitize } from "@/lib/sanitize";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -116,11 +117,10 @@ export default async function HelpCategoryPage({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* content is author-supplied HTML in AtomCMS (rendered raw with {!! !!}). */}
|
||||
{/* content is author-supplied HTML — sanitised server-side. */}
|
||||
<div
|
||||
style={{ lineHeight: 1.7 }}
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: author-supplied help content, matches AtomCMS Blade
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(content) }}
|
||||
/>
|
||||
|
||||
{hasButton ? (
|
||||
|
||||
+2
-6
@@ -51,12 +51,8 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
||||
return (
|
||||
<html lang={locale} className={`app ${nunito.variable} ${pixelFont.variable}`}>
|
||||
<head>
|
||||
{/* Apply the saved/default theme before first paint to avoid a flash. */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `try{var s=localStorage.getItem('theme');if(s==='dark'||(!s&&${defaultDark}))document.documentElement.classList.add('dark');var nc=localStorage.getItem('navbarColor'),nt=localStorage.getItem('navbarTextColor');if(nc)document.documentElement.style.setProperty('--color-navbar',nc);if(nt)document.documentElement.style.setProperty('--color-navbar-text',nt);}catch(e){}`,
|
||||
}}
|
||||
/>
|
||||
<meta name="theme-default-dark" content={String(defaultDark)} />
|
||||
<script src="/scripts/theme-init.js" />
|
||||
</head>
|
||||
<body
|
||||
className="flex min-h-screen flex-col site-bg"
|
||||
|
||||
Reference in new issue
Block a user