Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/api/auth/siwe/verify/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export async function POST(req: Request): Promise<NextResponse> {
}
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
// JSON の `null` / 数値 / 文字列も parse は成功する。object でなければ欄を読む前に 400 で返す
// (null のまま欄を読むと TypeError → 500 + Sentry event になっていた・第 6 回 B-R6f)。
if (cappedBody.value === null || typeof cappedBody.value !== 'object') {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
const body = cappedBody.value as { message?: unknown; signature?: unknown };

// domain 束縛はサーバ制御の許可リストで判定する (Host ヘッダは偽装可能なので使わない —
Expand Down
5 changes: 5 additions & 0 deletions app/api/billing/settle/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export async function POST(req: Request): Promise<NextResponse> {
{ status: 400 },
);
}
// JSON の `null` も parse は成功し、分解代入で TypeError → 500 になっていた (第 6 回 B-R6f)。money-path なので
// null だけを 400 にする (文字列/数値の本文は従来どおり invalid_chain・pro/subscribe と同じ線)。
if (capped.value === null) {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
// 既存の分解代入・検証順序は不変に保つ (掟12: money-path は追加のみ)。
const body = capped.value as {
txHash?: unknown;
Expand Down
5 changes: 5 additions & 0 deletions app/api/csv-pass/relay/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export async function POST(req: Request): Promise<NextResponse> {
} catch {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
// JSON.parse は `null` / 数値 / 文字列も返す。`null` のまま raw.chainId を読むと TypeError → 500 + Sentry
// になっていた (第 6 回 B-R6f)。object 以外は欄不足と同じ invalid_payload で 400 (relay/status も object 以外を拒否する・register/claim は invalid_body で配列も拒否)。
if (raw === null || typeof raw !== 'object') {
return NextResponse.json({ ok: false, error: 'invalid_payload' }, { status: 400 });
}

if (typeof raw.chainId !== 'number' || !Number.isInteger(raw.chainId)) {
return NextResponse.json({ ok: false, error: 'invalid_payload' }, { status: 400 });
Expand Down
5 changes: 5 additions & 0 deletions app/api/freee/mapping/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export async function POST(req: Request): Promise<NextResponse> {
} catch {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
// JSON の `null` / 数値 / 文字列も parse は成功する。object でなければ欄を読む前に 400 で返す
// (null のまま欄を読むと TypeError → 500 + Sentry event になっていた・第 6 回 B-R6f)。
if (body === null || typeof body !== 'object') {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
if (
typeof body.accountItemId !== 'number' ||
typeof body.taxCode !== 'number'
Expand Down
5 changes: 5 additions & 0 deletions app/api/freee/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export async function POST(req: Request): Promise<NextResponse> {
} catch {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
// JSON の `null` / 数値 / 文字列も parse は成功する。object でなければ欄を読む前に 400 で返す
// (null のまま欄を読むと TypeError → 500 + Sentry event になっていた・第 6 回 B-R6f)。
if (body === null || typeof body !== 'object') {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
const entries = Array.isArray(body.entries) ? (body.entries as HistoryEntry[]) : [];
if (entries.length === 0) {
return NextResponse.json({ ok: false, error: 'no_entries' }, { status: 400 });
Expand Down
5 changes: 5 additions & 0 deletions app/api/relay/jpyc/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ export async function POST(req: Request): Promise<NextResponse> {
} catch {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
// JSON.parse は `null` / 数値 / 文字列も返す。`null` のまま raw.chainId を読むと TypeError → 500 + Sentry
// になっていた (第 6 回 B-R6f)。object 以外は欄不足と同じ invalid_payload で 400 (relay/status も object 以外を拒否する・register/claim は invalid_body で配列も拒否)。
if (raw === null || typeof raw !== 'object') {
return NextResponse.json({ ok: false, error: 'invalid_payload' }, { status: 400 });
}

if (typeof raw.chainId !== 'number' || !Number.isInteger(raw.chainId)) {
return NextResponse.json({ ok: false, error: 'invalid_payload' }, { status: 400 });
Expand Down
12 changes: 12 additions & 0 deletions tests/app/api/auth-siwe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,18 @@ describe('SIWE routes', () => {
expect(h.kvSet).toHaveBeenCalledOnce();
});

it('verify: 本文が JSON の null → 400 invalid_json (verify_failed の 503 + Sentry にしない・B-R6f)', async () => {
h.kvConfigured = true;
const headers = new Headers({ 'content-type': 'application/json' });
headers.set('sec-fetch-site', 'same-origin');
const res = await verifyPOST(new Request('http://localhost/api/auth/siwe/verify', {
method: 'POST', headers, body: 'null',
}));
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ ok: false, error: 'invalid_json' });
expect(h.kvSet).not.toHaveBeenCalled();
});

it('verify: JSON-shaped bytes without Content-Type still fail before session writes', async () => {
h.kvConfigured = true;
const res = await verifyPOST(new Request('http://localhost/api/auth/siwe/verify', {
Expand Down
6 changes: 6 additions & 0 deletions tests/app/api/billing-settle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ describe('POST /api/billing/settle', () => {
expect(await res.json()).toMatchObject({ error: 'invalid_json' });
});

it('本文が JSON の null → 400 invalid_json (分解代入の TypeError で 500 にしない・B-R6f)', async () => {
const res = await POST(req(null));
expect(res.status).toBe(400);
expect(await res.json()).toMatchObject({ error: 'invalid_json' });
});

it('body 上限超過 → JSON parse 前に 413 payload_too_large', async () => {
const huge = new Request('http://localhost/api/billing/settle', {
method: 'POST',
Expand Down
6 changes: 6 additions & 0 deletions tests/app/api/csv-pass-relay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ describe('POST /api/csv-pass/relay — route 固有検証', () => {
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ ok: false, error: 'invalid_payload' });
});

it('本文が JSON の null → 400 invalid_payload (TypeError の 500 にしない・B-R6f)', async () => {
const res = await POST(req(null));
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ ok: false, error: 'invalid_payload' });
});
});

describe('POST /api/csv-pass/relay — relay 結果の応答整形 + guards 配線', () => {
Expand Down
17 changes: 17 additions & 0 deletions tests/app/api/freee-routes-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ describe('GET/POST /api/freee/mapping (実グルー)', () => {
expect(h.store.has(mappingKey)).toBe(false);
});

it('POST: 本文が JSON の null → 400 invalid_json (TypeError の 500 にしない・B-R6f)', async () => {
seedSession();
seedFreeeConnected();
const res = await mappingPOST(req('http://localhost/api/freee/mapping', null));
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ ok: false, error: 'invalid_json' });
});

it('sync: 本文が JSON の null → 400 invalid_json (TypeError の 500 にしない・B-R6f)', async () => {
seedSession();
seedFreeeConnected();
seedMapping();
const res = await syncPOST(req('http://localhost/api/freee/sync', null));
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ ok: false, error: 'invalid_json' });
});

it('POST: 不正 body → 400 invalid_mapping', async () => {
seedSession();
seedFreeeConnected();
Expand Down
9 changes: 9 additions & 0 deletions tests/app/api/relay-jpyc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ describe('POST /api/relay/jpyc (env-gate)', () => {
const body = (await res.json()) as { error?: string };
expect(body.error).toBe('invalid_payload');
});

it('本文が JSON の null → 400 invalid_payload (raw.chainId の TypeError で 500 にしない・B-R6f)', async () => {
vi.stubEnv(EIP3009_ON, '1');
vi.stubEnv('RELAYER_PRIVATE_KEY', DUMMY_RELAYER_KEY);
const mod = await import('@/app/api/relay/jpyc/route');
const res = await mod.POST(req(null));
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ ok: false, error: 'invalid_payload' });
});
});

// recover (forwarder) × a1 (usage fee) の排他は resolver (jpycForwarderFor) で graceful に解決する:
Expand Down
Loading