chore: patch deps, CSP style nonces, otplib 13, and PR CI
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
87bda8dbef
commit
2ff08e5127
11 files changed
+239
-160
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ export default async function RootLayout({
|
||||
style={{ backgroundColor: "var(--color-background)" }}
|
||||
>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeVars />
|
||||
<ThemeVars nonce={nonce} />
|
||||
<ViewTransitions>
|
||||
{children}
|
||||
</ViewTransitions>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
// Injects the DB-driven CSS custom properties into :root / html.dark.
|
||||
// Readable (WCAG) text colors are derived centrally in themePaletteCss.
|
||||
export async function ThemeVars() {
|
||||
export async function ThemeVars({ nonce }: { nonce?: string } = {}) {
|
||||
const g = (k: string, d: string) => siteSettings.get(k, d);
|
||||
const [
|
||||
primary,
|
||||
@@ -175,10 +175,13 @@ export async function ThemeVars() {
|
||||
return (
|
||||
<>
|
||||
{googleHref ? <link rel="stylesheet" href={googleHref} /> : null}
|
||||
<style dangerouslySetInnerHTML={{ __html: css }} />
|
||||
<style nonce={nonce} dangerouslySetInnerHTML={{ __html: css }} />
|
||||
{customCss?.trim() ? (
|
||||
// Staff-authored custom CSS (housekeeping → Theme). Trusted input.
|
||||
<style dangerouslySetInnerHTML={{ __html: customCss }} />
|
||||
<style
|
||||
nonce={nonce}
|
||||
dangerouslySetInnerHTML={{ __html: customCss }}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
+28
-8
@@ -1,13 +1,29 @@
|
||||
import { authenticator } from "otplib";
|
||||
import {
|
||||
createGuardrails,
|
||||
generateSecret,
|
||||
generateSync,
|
||||
generateURI,
|
||||
verifySync,
|
||||
} from "otplib";
|
||||
|
||||
// Laravel Fortify uses pragmarx/google2fa: HMAC-SHA1, 6 digits, 30s period.
|
||||
// otplib already defaults to SHA1/6/30; window=1 tolerates one step of skew.
|
||||
authenticator.options = { window: 1 };
|
||||
// Laravel Fortify / pragmarx google2fa: HMAC-SHA1, 6 digits, 30s period.
|
||||
// Allow legacy secrets shorter than otplib v13's default 16-byte minimum
|
||||
// (many existing AtomCMS/Fortify secrets decode to ~10 bytes).
|
||||
const guardrails = createGuardrails({ MIN_SECRET_BYTES: 10 });
|
||||
|
||||
// ±1 time-step of clock skew (30s period → epochTolerance 30).
|
||||
const EPOCH_TOLERANCE = 30;
|
||||
|
||||
/** Verify a 6-digit TOTP code against a base32 secret. */
|
||||
export function verifyTotp(token: string, secret: string): boolean {
|
||||
try {
|
||||
return authenticator.check(token, secret);
|
||||
const result = verifySync({
|
||||
secret,
|
||||
token,
|
||||
epochTolerance: EPOCH_TOLERANCE,
|
||||
guardrails,
|
||||
});
|
||||
return result.valid === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -15,12 +31,12 @@ export function verifyTotp(token: string, secret: string): boolean {
|
||||
|
||||
/** Current TOTP code for a secret (used in tests / tooling). */
|
||||
export function generateTotp(secret: string): string {
|
||||
return authenticator.generate(secret);
|
||||
return generateSync({ secret, guardrails });
|
||||
}
|
||||
|
||||
/** Generate a fresh base32 secret for enrolling a new authenticator. */
|
||||
export function generateTotpSecret(): string {
|
||||
return authenticator.generateSecret();
|
||||
return generateSecret();
|
||||
}
|
||||
|
||||
/** otpauth:// URI for provisioning a QR code. */
|
||||
@@ -29,5 +45,9 @@ export function totpKeyUri(
|
||||
accountName: string,
|
||||
issuer: string,
|
||||
): string {
|
||||
return authenticator.keyuri(accountName, issuer, secret);
|
||||
return generateURI({
|
||||
issuer,
|
||||
label: accountName,
|
||||
secret,
|
||||
});
|
||||
}
|
||||
+3
-1
@@ -13,7 +13,9 @@ describe("csp", () => {
|
||||
expect(csp).toContain("script-src");
|
||||
expect(csp).toContain("'nonce-testNonce123'");
|
||||
expect(csp).not.toMatch(/script-src[^;]*'unsafe-inline'/);
|
||||
expect(csp).toContain("style-src 'self' 'unsafe-inline'");
|
||||
expect(csp).toContain("style-src 'self' 'nonce-testNonce123'");
|
||||
expect(csp).not.toMatch(/style-src(?!-attr)[^;]*'unsafe-inline'/);
|
||||
expect(csp).toContain("style-src-attr 'unsafe-inline'");
|
||||
expect(csp).toContain("https://challenges.cloudflare.com");
|
||||
expect(csp).toContain("https://cdn.jsdelivr.net");
|
||||
});
|
||||
|
||||
+11
-5
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Build a Content-Security-Policy value.
|
||||
* Scripts: nonce for Next.js / first-party inline; host allowlists for captcha,
|
||||
* Cloudflare Insights, and TinyMCE CDN. style-src keeps 'unsafe-inline' for
|
||||
* theme CSS variables (ThemeVars) — nonce styles are a follow-up.
|
||||
* Scripts/styles: per-request nonce for Next.js and ThemeVars inline tags.
|
||||
* style-src-attr keeps 'unsafe-inline' so React `style={{…}}` attributes work.
|
||||
*/
|
||||
export function buildContentSecurityPolicy(nonce: string): string {
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
@@ -17,14 +16,21 @@ export function buildContentSecurityPolicy(nonce: string): string {
|
||||
...(isDev ? ["'unsafe-eval'"] : []),
|
||||
].join(" ");
|
||||
|
||||
const styleSrc = [
|
||||
"'self'",
|
||||
`'nonce-${nonce}'`,
|
||||
"https://fonts.googleapis.com",
|
||||
].join(" ");
|
||||
|
||||
return [
|
||||
"default-src 'self'",
|
||||
`script-src ${scriptSrc}`,
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
`style-src ${styleSrc}`,
|
||||
"style-src-attr 'unsafe-inline'",
|
||||
"img-src 'self' data: blob: https:",
|
||||
"frame-src 'self' https://challenges.cloudflare.com https://www.google.com/recaptcha/",
|
||||
"connect-src 'self' https: wss:",
|
||||
"font-src 'self' data:",
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
|
||||
@@ -11,8 +11,6 @@ import { prisma } from "./prisma";
|
||||
// Re-export PERMS from the standalone file (safe for client components)
|
||||
export { PERMS } from "./permission-slugs";
|
||||
|
||||
import { PERMS } from "./permission-slugs";
|
||||
|
||||
// ── Permission Set ──────────────────────────────────────────────────
|
||||
|
||||
export type { PermissionSet } from "@/types/admin";
|
||||
|
||||
Reference in new issue
Block a user