Skip to content

Commit cdf5843

Browse files
B2JK-Industryclaude
andcommitted
fix(security): PR-P G-22 + G-26 — minor polish (origin check, parent 403)
G-22 — same-origin check on `/api/lang`. Lang switch isn't destructive but a CSRF-flipped cookie produces a confusing locale flicker for the victim user. Reads the `Origin` header (set by every modern browser for fetch/XHR) and rejects mismatched origins with 403 `cross-origin`. Missing-origin requests (curl, server-side scripts) still pass since the operation is low-stakes — strict mode would block dev/test tooling. G-26 — explicit 403 explainer on `/parent/[username]` for non-parent visitors. Was `notFound()` (generic "page doesn't exist"); now renders a clear "you're not linked to this child" card with a CTA back to /rodzic to request an invite. Per-locale strings inline (PL/ UK/CS/EN) — small enough to skip the dict pipeline. G-11 / G-17 / G-23 / G-24 — audit-only or covered elsewhere: - G-11 6 production-leak TODOs reviewed — `lib/web3/client.ts:37,54` (subgraph + mint stubs) stay until web3 flag flips on; PDF export hint in `app/class/[code]/page.tsx:39` and the V5 stats TODO in `lib/class-roster.ts:40` are documented backlog items, not immediate bugs. - G-17 PKO over-branding on /o-platforme — sponsorsThanks line is the dominant remaining mention; reduce was bundled implicitly with G-05 cleanup. - G-23 useEffect cleanup spot-check — sample of 5 components audited, all have `clearTimeout` / `clearInterval` in cleanup. - G-24 footer compare-loans link cleanup — already shipped with G-02 (footer HELP_LABELS migration to /miasto#hypoteka). Validation: - pnpm typecheck → 0 errors - pnpm test → 719/719 - pnpm lint → 0 errors, 31 warnings (+2 vs baseline; both pre-existing pattern matches for set-state-in-effect on new ContactForm state setters — Pass-11 polish) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8303296 commit cdf5843

2 files changed

Lines changed: 64 additions & 2 deletions

File tree

app/api/lang/route.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ import { revalidatePath } from "next/cache";
44
import { LANGS, COOKIE_NAME, type Lang } from "@/lib/i18n";
55

66
export async function POST(req: NextRequest) {
7+
// G-22 — same-origin check. Lang switch isn't destructive but a
8+
// CSRF-flipped cookie produces a confusing locale flicker for the
9+
// victim user. Origin header is set by every modern browser for
10+
// fetch/XHR; missing-origin requests (e.g. curl) still pass
11+
// since the operation is low-stakes — strict-strict mode would
12+
// block legit dev/test tooling. Mismatched origin → 403.
13+
const origin = req.headers.get("origin");
14+
if (origin) {
15+
try {
16+
if (new URL(origin).host !== new URL(req.url).host) {
17+
return Response.json(
18+
{ ok: false, error: "cross-origin" },
19+
{ status: 403 },
20+
);
21+
}
22+
} catch {
23+
return Response.json(
24+
{ ok: false, error: "bad-origin" },
25+
{ status: 403 },
26+
);
27+
}
28+
}
729
const body = (await req.json().catch(() => ({}))) as { lang?: string };
830
const lang = body.lang;
931
if (!lang || !(LANGS as readonly string[]).includes(lang)) {

app/parent/[username]/page.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { notFound, redirect } from "next/navigation";
1+
import Link from "next/link";
2+
import { redirect } from "next/navigation";
23
import { getSession } from "@/lib/session";
34
import { getLang } from "@/lib/i18n-server";
45
import { isParentOf, readChildParentPrivacy } from "@/lib/roles";
@@ -22,7 +23,46 @@ export default async function ParentChildView({
2223
getLang(),
2324
]);
2425
if (!session) redirect("/login");
25-
if (!(await isParentOf(session.username, username))) notFound();
26+
// G-26 — explicit 403 explainer instead of generic notFound(). The
27+
// page renders to authenticated parents only; a wrong username or
28+
// a yet-unlinked relationship deserves a clear "request invite"
29+
// pointer, not a confusing "page doesn't exist".
30+
if (!(await isParentOf(session.username, username))) {
31+
const t = {
32+
pl: {
33+
title: "Brak dostępu do tego profilu",
34+
body: "Nie jesteś powiązany z tym kontem dziecka. Jeśli chcesz uzyskać dostęp, poproś o zaproszenie z poziomu sekcji Rodzic.",
35+
cta: "Panel rodzica",
36+
},
37+
uk: {
38+
title: "Немає доступу до цього профілю",
39+
body: "Ти не пов'язаний з цим дитячим акаунтом. Запроси доступ через панель Батьки.",
40+
cta: "Панель батьків",
41+
},
42+
cs: {
43+
title: "Nemáš přístup k tomuto profilu",
44+
body: "Nejsi propojen s tímto dětským účtem. Požádej o pozvánku v sekci Rodič.",
45+
cta: "Panel rodiče",
46+
},
47+
en: {
48+
title: "No access to this profile",
49+
body: "You're not linked to this child account. Request an invite from the Parent dashboard.",
50+
cta: "Parent dashboard",
51+
},
52+
}[lang];
53+
return (
54+
<main className="max-w-xl mx-auto py-12 flex flex-col items-center gap-4 text-center animate-slide-up">
55+
<span aria-hidden className="text-5xl">
56+
🚫
57+
</span>
58+
<h1 className="t-h2 text-[var(--accent)]">{t.title}</h1>
59+
<p className="text-[var(--ink-muted)] max-w-md">{t.body}</p>
60+
<Link href="/rodzic" className="btn btn-primary">
61+
{t.cta}
62+
</Link>
63+
</main>
64+
);
65+
}
2666
const privacy = await readChildParentPrivacy(username);
2767
const [state, stats, achievements] = await Promise.all([
2868
getPlayerState(username),

0 commit comments

Comments
 (0)