Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c518890fc3 | ||
|
|
1925676fc9 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tallynote",
|
"name": "tallynote",
|
||||||
"version": "1.1.9",
|
"version": "1.1.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@9.0.6",
|
"packageManager": "pnpm@9.0.6",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
root=$(cd "$(dirname "$0")/.." && pwd)
|
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
bash -n "$root/install.sh" "$root/scripts/tallynote-update.sh"
|
bash -n "$root/install.sh" "$root/scripts/tallynote-update.sh"
|
||||||
|
grep -Eq '^RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK$' "$root/systemd/tallynote.service"
|
||||||
|
grep -Eq '^RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK$' "$root/systemd/tallynote-update.service"
|
||||||
output=$(bash "$root/install.sh" --dry-run --release-base-url https://releases.example.test/releases)
|
output=$(bash "$root/install.sh" --dry-run --release-base-url https://releases.example.test/releases)
|
||||||
grep -q 'dry-run' <<<"$output"
|
grep -q 'dry-run' <<<"$output"
|
||||||
grep -q '\[阶段\] 检查运行环境' <<<"$output"
|
grep -q '\[阶段\] 检查运行环境' <<<"$output"
|
||||||
|
|||||||
+10
-3
@@ -590,6 +590,13 @@ function conflict(database: DatabaseContext, id: string): never {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
||||||
|
// Helmet's defaults include `upgrade-insecure-requests`, HSTS, COOP and
|
||||||
|
// Origin-Agent-Cluster. Those headers are appropriate for HTTPS, but an
|
||||||
|
// explicitly opted-in HTTP deployment must remain HTTP all the way through
|
||||||
|
// the asset graph; otherwise browsers upgrade `/assets/*` to HTTPS and the
|
||||||
|
// plain HTTP listener appears as a blank page. Keep the transport-sensitive
|
||||||
|
// headers protocol-aware while retaining the other hardening headers.
|
||||||
|
const secureOrigin = config.publicOrigin.startsWith("https:");
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: config.isProduction ? { level: "info", redact: ["req.headers.cookie", "req.headers.x-csrf-token", "password", "temporaryPassword"] } : false,
|
logger: config.isProduction ? { level: "info", redact: ["req.headers.cookie", "req.headers.x-csrf-token", "password", "temporaryPassword"] } : false,
|
||||||
// Fastify's runtime accepts a numeric hop count, while its v5 typings do
|
// Fastify's runtime accepts a numeric hop count, while its v5 typings do
|
||||||
@@ -604,10 +611,10 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
|||||||
|
|
||||||
await app.register(cookie);
|
await app.register(cookie);
|
||||||
await app.register(helmet, {
|
await app.register(helmet, {
|
||||||
...(config.isLocalOrigin ? { hsts: false } : {}),
|
...(!secureOrigin || config.isLocalOrigin ? { hsts: false } : {}),
|
||||||
frameguard: { action: "deny" },
|
frameguard: { action: "deny" },
|
||||||
referrerPolicy: { policy: "no-referrer" },
|
referrerPolicy: { policy: "no-referrer" },
|
||||||
crossOriginOpenerPolicy: { policy: "same-origin" },
|
...(secureOrigin ? { crossOriginOpenerPolicy: { policy: "same-origin" }, originAgentCluster: true } : { crossOriginOpenerPolicy: false, originAgentCluster: false }),
|
||||||
crossOriginResourcePolicy: { policy: "same-origin" },
|
crossOriginResourcePolicy: { policy: "same-origin" },
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
directives: {
|
directives: {
|
||||||
@@ -618,7 +625,7 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
|||||||
"frame-ancestors": ["'none'"],
|
"frame-ancestors": ["'none'"],
|
||||||
"base-uri": ["'none'"],
|
"base-uri": ["'none'"],
|
||||||
"form-action": ["'self'"],
|
"form-action": ["'self'"],
|
||||||
...(config.isLocalOrigin ? { "upgrade-insecure-requests": null } : {}),
|
...(!secureOrigin ? { "upgrade-insecure-requests": null } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
|||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
CapabilityBoundingSet=
|
CapabilityBoundingSet=
|
||||||
AmbientCapabilities=
|
AmbientCapabilities=
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
# Keep the updater compatible with the same Node/libuv interface discovery
|
||||||
|
# path while retaining an explicit socket-family allowlist.
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
PrivateDevices=true
|
PrivateDevices=true
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ ProtectSystem=strict
|
|||||||
InaccessiblePaths=/opt/tallynote/.update-work
|
InaccessiblePaths=/opt/tallynote/.update-work
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
PrivateDevices=true
|
PrivateDevices=true
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
# Fastify logs the addresses of wildcard listeners. Node's libuv uses the
|
||||||
|
# Linux netlink family while enumerating interfaces for that log message.
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
ProtectKernelModules=true
|
ProtectKernelModules=true
|
||||||
ProtectKernelLogs=true
|
ProtectKernelLogs=true
|
||||||
|
|||||||
@@ -87,6 +87,48 @@ describe("TallyNote API", () => {
|
|||||||
expect(missing.json().error.requestId).toBeTruthy();
|
expect(missing.json().error.requestId).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("显式允许的公网 HTTP 不会把静态资源升级到 HTTPS", async () => {
|
||||||
|
const publicHttpConfig = {
|
||||||
|
...config,
|
||||||
|
publicOrigin: "http://192.0.2.10:3999",
|
||||||
|
isLocalOrigin: false,
|
||||||
|
allowInsecureHttp: true,
|
||||||
|
cookieSecure: false,
|
||||||
|
};
|
||||||
|
const publicHttpApp = await buildApp(database, publicHttpConfig);
|
||||||
|
try {
|
||||||
|
const response = await publicHttpApp.inject({ method: "GET", url: "/health" });
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.headers["content-security-policy"]).not.toContain("upgrade-insecure-requests");
|
||||||
|
expect(response.headers["strict-transport-security"]).toBeUndefined();
|
||||||
|
expect(response.headers["cross-origin-opener-policy"]).toBeUndefined();
|
||||||
|
expect(response.headers["origin-agent-cluster"]).toBeUndefined();
|
||||||
|
} finally {
|
||||||
|
await publicHttpApp.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("HTTPS 仍保留传输安全响应头", async () => {
|
||||||
|
const secureConfig = {
|
||||||
|
...config,
|
||||||
|
publicOrigin: "https://example.test:3999",
|
||||||
|
isLocalOrigin: false,
|
||||||
|
allowInsecureHttp: false,
|
||||||
|
cookieSecure: true,
|
||||||
|
};
|
||||||
|
const secureApp = await buildApp(database, secureConfig);
|
||||||
|
try {
|
||||||
|
const response = await secureApp.inject({ method: "GET", url: "/health" });
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.headers["content-security-policy"]).toContain("upgrade-insecure-requests");
|
||||||
|
expect(response.headers["strict-transport-security"]).toContain("max-age=");
|
||||||
|
expect(response.headers["cross-origin-opener-policy"]).toBe("same-origin");
|
||||||
|
expect(response.headers["origin-agent-cluster"]).toBe("?1");
|
||||||
|
} finally {
|
||||||
|
await secureApp.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("拒绝没有 Origin 的写请求", async () => {
|
it("拒绝没有 Origin 的写请求", async () => {
|
||||||
const response = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "x", password: "x" } });
|
const response = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "x", password: "x" } });
|
||||||
expect(response.statusCode).toBe(403);
|
expect(response.statusCode).toBe(403);
|
||||||
|
|||||||
Reference in New Issue
Block a user