fix(security): harden BYO API header against SSRF and input abuse (#33)

* fix(security): harden BYO API header against SSRF and input abuse

- Add lib/validateUrl.ts with HTTPS-only + public-IP enforcement,
  provider allowlist, IPv6 rejection, and userinfo-in-URL blocking.
- Add lib/byoHeaders.ts — single source of truth for client-side BYO
  header construction (deduplicates app/page.tsx & app/play/page.tsx).
- config.ts: validate BYO endpoints via isPublicUrl(), cap header at
  2 KB, truncate apiKey/model strings, sanitize log output.
- fetchWithRetry: default redirect to "manual" to block 302-to-intranet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): address Copilot review — trim endpoint, strip control chars, drop unused import

- safeEndpoint: trim whitespace before URL validation
- safeString: strip ASCII control characters to prevent header injection
- play/page.tsx: remove unused BYO_STORAGE_KEY import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zonghao Yuan
2026-06-05 00:23:35 +08:00
committed by GitHub
parent bc8f47e601
commit c30d11d60b
6 changed files with 163 additions and 65 deletions
+1 -18
View File
@@ -11,6 +11,7 @@ import {
type Gender,
} from "@/lib/options";
import { readStoredTtsConfig } from "@/lib/clientTtsConfig";
import { BYO_STORAGE_KEY, getByoHeaders } from "@/lib/byoHeaders";
import { TtsKeyModal } from "@/components/TtsKeyModal";
/* ============================================================================
@@ -749,8 +750,6 @@ const DEFAULT_BYO: ByoApiConfig = {
},
};
const BYO_STORAGE_KEY = "infiplot:byoApi";
function normalizeByoSection(
s: unknown,
providers: Record<string, ByoProvider>,
@@ -788,22 +787,6 @@ function loadByoConfig(): ByoApiConfig {
}
}
function getByoHeaders(): Record<string, string> {
if (typeof window === "undefined") return {};
try {
const raw = localStorage.getItem(BYO_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed.llm?.enabled || parsed.painter?.enabled) {
return { "x-byo-api": raw };
}
}
} catch {
/* ignore */
}
return {};
}
/* ---------- typewriter ---------- */
// 父组件持有当前 phrase 的索引(这样 start() 不输入时能用当前闪动的那句
+4 -29
View File
@@ -35,25 +35,9 @@ import type {
VisionResponse,
} from "@infiplot/types";
import { track } from "@/lib/analytics";
import { getByoHeaders, isByoActive } from "@/lib/byoHeaders";
const MUTED_STORAGE_KEY = "infiplot:muted";
const BYO_STORAGE_KEY = "infiplot:byoApi";
function getByoHeaders(): Record<string, string> {
if (typeof window === "undefined") return {};
try {
const raw = localStorage.getItem(BYO_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed.llm?.enabled || parsed.painter?.enabled) {
return { "x-byo-api": raw };
}
}
} catch {
/* ignore */
}
return {};
}
// Consecutive silent (no-audio) beats before we surface the BYO-key nudge to a
// non-BYO, unmuted player. Set high enough that one transient miss won't trip
@@ -1298,16 +1282,7 @@ function PlayInner() {
// ── Render ────────────────────────────────────────────────────────────
if (error) {
const isByoActive = typeof window !== "undefined" && (() => {
try {
const raw = localStorage.getItem(BYO_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
return parsed.llm?.enabled || parsed.painter?.enabled;
}
} catch {}
return false;
})();
const byoOn = isByoActive();
return (
<div className="min-h-screen flex flex-col items-center justify-center px-8">
@@ -1318,14 +1293,14 @@ function PlayInner() {
<p className="font-serif italic text-clay-900 text-lg leading-[1.7] mb-6">
{error}
</p>
{isByoActive && (
{byoOn && (
<p className="font-sans text-xs text-ember-600 mb-10 leading-relaxed">
API API KeyEndpoint Model
</p>
)}
<Link
href="/"
className={"text-[10px] smallcaps text-clay-700 hover:text-ember-500 transition-colors inline-flex items-center gap-3" + (isByoActive ? "" : " mt-4")}
className={"text-[10px] smallcaps text-clay-700 hover:text-ember-500 transition-colors inline-flex items-center gap-3" + (byoOn ? "" : " mt-4")}
>
<i className="fa-solid fa-arrow-left text-[9px]" />