fix(rc5): harden frontend runtime and diagnostics
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
redactLogText,
|
||||
redactYaml,
|
||||
redactYamlFallback,
|
||||
sanitizeUrlValue
|
||||
sanitizeUrlValue,
|
||||
} from "../src/lib/redaction";
|
||||
|
||||
const MACHINE_TOKEN = "hy2xs-machine-token-must-not-leak";
|
||||
@@ -58,7 +58,10 @@ describe("редакция серверного конфига", () => {
|
||||
});
|
||||
|
||||
test("результат остаётся валидным YAML", () => {
|
||||
const parsed = Bun.YAML.parse(redactYaml(SERVER_CONFIG)) as Record<string, unknown>;
|
||||
const parsed = Bun.YAML.parse(redactYaml(SERVER_CONFIG)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(parsed.listen).toBe("0.0.0.0:443");
|
||||
const auth = parsed.auth as Record<string, unknown>;
|
||||
const http = auth.http as Record<string, unknown>;
|
||||
@@ -69,7 +72,10 @@ describe("редакция серверного конфига", () => {
|
||||
});
|
||||
|
||||
test("несекретные поля сохраняются: диагностика должна оставаться полезной", () => {
|
||||
const parsed = Bun.YAML.parse(redactYaml(SERVER_CONFIG)) as Record<string, unknown>;
|
||||
const parsed = Bun.YAML.parse(redactYaml(SERVER_CONFIG)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const acme = parsed.acme as Record<string, unknown>;
|
||||
expect(acme.email).toBe("admin@example.com");
|
||||
expect(acme.ca).toBe("letsencrypt");
|
||||
@@ -92,7 +98,10 @@ describe("редакция серверного конфига", () => {
|
||||
test("вложенное дерево под секретным ключом вырезается целиком", () => {
|
||||
const yaml = `acme:\n dns:\n name: cloudflare\n config:\n api_token: leak-me\n zone: example.com\n`;
|
||||
const parsed = Bun.YAML.parse(redactYaml(yaml)) as Record<string, unknown>;
|
||||
const dns = (parsed.acme as Record<string, unknown>).dns as Record<string, unknown>;
|
||||
const dns = (parsed.acme as Record<string, unknown>).dns as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const cfg = dns.config as Record<string, unknown>;
|
||||
expect(cfg.api_token).toBe(REDACTED);
|
||||
// acme.dns.config — карта, где секретны все значения, а не только ключ.
|
||||
@@ -158,7 +167,7 @@ describe("редакция env-артефактов", () => {
|
||||
"HY2_AUTH_MODE=http",
|
||||
`HY2_AUTH_URL=http://127.0.0.1:8080/internal/hysteria/auth?access_token=${MACHINE_TOKEN}`,
|
||||
`HY2_OBFS_PASSWORD=${OBFS_PASSWORD}`,
|
||||
"HY2_PORT=443"
|
||||
"HY2_PORT=443",
|
||||
].join("\n");
|
||||
|
||||
// Регрессия: HY2_AUTH_URL не подходит ни под один key-маркер, но несёт
|
||||
@@ -166,7 +175,9 @@ describe("редакция env-артефактов", () => {
|
||||
test("секрет внутри URL-значения вырезается, даже если имя ключа несекретное", () => {
|
||||
const redacted = redactEnv(POST_INSTALL);
|
||||
expect(redacted).not.toContain(MACHINE_TOKEN);
|
||||
expect(redacted).toContain("HY2_AUTH_URL=http://127.0.0.1:8080/internal/hysteria/auth");
|
||||
expect(redacted).toContain(
|
||||
"HY2_AUTH_URL=http://127.0.0.1:8080/internal/hysteria/auth"
|
||||
);
|
||||
});
|
||||
|
||||
test("ключи-секреты вырезаются по имени", () => {
|
||||
@@ -180,13 +191,40 @@ describe("редакция env-артефактов", () => {
|
||||
expect(redacted).toContain("HY2_PORT=443");
|
||||
});
|
||||
|
||||
test("boolean-флаг принудительной смены пароля остаётся диагностируемым", () => {
|
||||
const flags = [
|
||||
"HY2XS_FORCE_PASSWORD_CHANGE=false",
|
||||
"HY2XS_FORCE_PASSWORD_CHANGE=true",
|
||||
].join("\n");
|
||||
|
||||
expect(redactEnv(flags)).toBe(flags);
|
||||
});
|
||||
|
||||
test("исключение для boolean-флага точное и не ослабляет защиту", () => {
|
||||
const input = [
|
||||
"HY2XS_FORCE_PASSWORD_CHANGE_SUFFIX=must-hide",
|
||||
"HY2XS_FORCE_PASSWORD_CHANGE=malformed-secret",
|
||||
"HY2XS_ADMIN_INITIAL_PASSWORD=initial-password",
|
||||
"HY2XS_ADMIN_CON_PASS=connection-password",
|
||||
`HY2XS_HYSTERIA_OBFS_PASSWORD=${OBFS_PASSWORD}`,
|
||||
].join("\n");
|
||||
const redacted = redactEnv(input);
|
||||
|
||||
expect(redacted).not.toContain("must-hide");
|
||||
expect(redacted).not.toContain("initial-password");
|
||||
expect(redacted).not.toContain("connection-password");
|
||||
expect(redacted).not.toContain(OBFS_PASSWORD);
|
||||
expect(redacted.match(/<redacted>/g)).toHaveLength(5);
|
||||
});
|
||||
|
||||
test("runtime-конфиг теряет все секреты", () => {
|
||||
const runtime = [
|
||||
"HY2XS_CONFIG_SCHEMA_VERSION=2",
|
||||
"HY2XS_FORCE_PASSWORD_CHANGE=false",
|
||||
`HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=${MACHINE_TOKEN}`,
|
||||
`HY2XS_HYSTERIA_OBFS_PASSWORD=${OBFS_PASSWORD}`,
|
||||
"HY2XS_ADMIN_CON_PASS=connection-password",
|
||||
"HY2XS_ADMIN_INITIAL_PASSWORD=initial-password"
|
||||
"HY2XS_ADMIN_INITIAL_PASSWORD=initial-password",
|
||||
].join("\n");
|
||||
|
||||
const redacted = redactEnv(runtime);
|
||||
@@ -195,16 +233,21 @@ describe("редакция env-артефактов", () => {
|
||||
expect(redacted).not.toContain("connection-password");
|
||||
expect(redacted).not.toContain("initial-password");
|
||||
expect(redacted).toContain("HY2XS_CONFIG_SCHEMA_VERSION=2");
|
||||
expect(redacted).toContain("HY2XS_FORCE_PASSWORD_CHANGE=false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("санитизация URL", () => {
|
||||
test("креденшелы в URL вырезаются", () => {
|
||||
expect(sanitizeUrlValue("https://user:hunter2@example.com/x")).not.toContain("hunter2");
|
||||
expect(
|
||||
sanitizeUrlValue("https://user:hunter2@example.com/x")
|
||||
).not.toContain("hunter2");
|
||||
});
|
||||
|
||||
test("секретные query-параметры вырезаются, остальные сохраняются", () => {
|
||||
const out = sanitizeUrlValue("https://example.com/x?access_token=abc&page=2");
|
||||
const out = sanitizeUrlValue(
|
||||
"https://example.com/x?access_token=abc&page=2"
|
||||
);
|
||||
expect(out).not.toContain("abc");
|
||||
expect(out).toContain("page=2");
|
||||
});
|
||||
@@ -245,7 +288,7 @@ describe("редакция журналов в diagnostics-бандле", () =>
|
||||
for (const line of [
|
||||
`starting with access_token=${MACHINE_TOKEN}`,
|
||||
`trafficStats secret=${MACHINE_TOKEN} rejected`,
|
||||
`obfs password: ${OBFS_PASSWORD}`
|
||||
`obfs password: ${OBFS_PASSWORD}`,
|
||||
]) {
|
||||
const out = redactLogText(line);
|
||||
expect(out).not.toContain(MACHINE_TOKEN);
|
||||
@@ -288,14 +331,22 @@ describe("маркеры секретных имён", () => {
|
||||
"cookie",
|
||||
"bearer",
|
||||
"signature",
|
||||
"passphrase"
|
||||
"passphrase",
|
||||
]) {
|
||||
expect(isSecretKey(key)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("не задевают обычные поля", () => {
|
||||
for (const key of ["listen", "type", "domains", "minPacketSize", "email", "ca", "dir"]) {
|
||||
for (const key of [
|
||||
"listen",
|
||||
"type",
|
||||
"domains",
|
||||
"minPacketSize",
|
||||
"email",
|
||||
"ca",
|
||||
"dir",
|
||||
]) {
|
||||
expect(isSecretKey(key)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user