feat: enforce browser security boundaries

This commit is contained in:
donghyeon-ka
2026-07-25 21:14:32 +09:00
parent 675603c3a2
commit 6f88915c7a
11 changed files with 163 additions and 1 deletions
+53
View File
@@ -0,0 +1,53 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SafeText } from "../../src/presentation/security/safe-text.jsx";
import { assertSafeConfigNames } from "../../src/contracts/env.js";
import { defineStorageKey } from "../../src/contracts/storage-keys.js";
import { projectTelemetryEvent } from "../../src/contracts/telemetry.js";
describe("browser security boundary", () => {
it("renders untrusted text without script or inline handler injection", () => {
render(
<SafeText value={'<img src=x onerror="window.compromised=true"><script>x</script>'} />,
);
expect(screen.getByText(/<img/)).toBeVisible();
expect(document.querySelector("script")).toBeNull();
expect(document.querySelector("[onerror]")).toBeNull();
});
it("rejects secret-like client configuration names", () => {
expect(() => assertSafeConfigNames({ PRIVATE_KEY: "not-public" })).toThrow();
});
it("rejects browser token storage registration", () => {
expect(() =>
defineStorageKey({
logicalName: "SESSION_TOKEN",
scope: "auth",
name: "session-token",
backend: "sessionStorage",
classification: "sensitive-forbidden",
schemaVersion: 1,
ttl: "session",
migration: "discard",
quotaFallback: "feature-disable",
}),
).toThrow();
});
it("drops raw URL/query/token telemetry attributes", () => {
const result = projectTelemetryEvent("api.request.failed", {
error_kind: "SERVER_FAILURE",
http_status_group: "5xx",
attempt_count_bucket: "1",
route_id: "APP_HOME",
raw_url: "https://api.test?token=private",
query_string: "token=private",
});
expect(result.success).toBe(true);
expect(JSON.stringify(result)).not.toMatch(/raw_url|query_string|private/);
});
});
+3
View File
@@ -0,0 +1,3 @@
export function Fixture({ value }) {
return <span>{value}</span>;
}
+5
View File
@@ -0,0 +1,5 @@
export function attachScript(source) {
const script = document.createElement("script");
script.src = source;
document.head.append(script);
}
+1
View File
@@ -0,0 +1 @@
export const execute = (source) => eval(source);
+3
View File
@@ -0,0 +1,3 @@
export function RawHtml({ value }) {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
}