mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-15 12:06:39 +07:00
security: устранить находки аудита, версия 1.1.7
Конфигурация и эксплуатационный слой: - nginx: client_max_body_size 64m — восстановление из бэкапа не работало вовсе, дефолтный лимит в 1 МБ резал дампы (реальный экспорт ~3.4 МБ) с 413 ещё до backend; - nginx: заголовки безопасности (nosniff, Referrer-Policy, Permissions-Policy) и полный Content-Security-Policy. Ради него inline-скрипт темы вынесен в public/theme-init.js; - nginx: limit_req на /api/ — перебор пароля Basic Auth шёл со скоростью сети, ни блокировки, ни задержки не было; - nginx: server_tokens off; - порт web привязан к 127.0.0.1, адрес настраивается через AURUM_BIND_ADDRESS (раньше слушался на всех интерфейсах); - backend работает от непривилегированного пользователя, no-new-privileges для backend и web; - дефолты пароля Postgres сведены к одному значению в обоих сервисах, в .env.example описана ловушка с инициализацией тома. Приложение: - версия убрана из неавторизованного /api/health и отдаётся как app_version в GET /api/settings; - /api/docs, /api/redoc и /api/openapi.json отключаются через AURUM_ENABLE_DOCS; - текст исключения при неудачном восстановлении уходит в лог, а не клиенту; - max_length для notes в схемах транзакций, активов и регулярных платежей (и в соответствующих полях UI); - срок «запомнить меня» сокращён с 30 дней до 7; - pytest обновлён до 9.0.3 (PYSEC-2026-1845), pytest-asyncio до 1.4.0; - CI: permissions contents:read для GITHUB_TOKEN.
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { HealthStatus } from "@/types";
|
||||
|
||||
export function fetchHealth() {
|
||||
return api.get<HealthStatus>("/health");
|
||||
}
|
||||
@@ -208,6 +208,7 @@ export function AssetFormModal({ open, onClose, asset }: AssetFormModalProps) {
|
||||
<Label htmlFor="asset-notes">{t("netWorth.form.notesLabel")}</Label>
|
||||
<Input
|
||||
id="asset-notes"
|
||||
maxLength={2000}
|
||||
value={form.notes}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, notes: event.target.value }))}
|
||||
/>
|
||||
|
||||
@@ -485,8 +485,11 @@ export function TransactionFormModal({ open, onClose, transaction }: Transaction
|
||||
|
||||
<div>
|
||||
<Label htmlFor="notes">{t("transactions.form.notesLabel")}</Label>
|
||||
{/* Matches the server's max_length on notes — without it an overlong
|
||||
note only fails on save, as an untranslated 422. */}
|
||||
<Input
|
||||
id="notes"
|
||||
maxLength={2000}
|
||||
value={form.notes}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, notes: event.target.value }))}
|
||||
/>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchHealth } from "@/api/health";
|
||||
|
||||
// The running version only changes on a deploy (a full page reload), so
|
||||
// there's no need to ever refetch within a session.
|
||||
export function useHealth() {
|
||||
return useQuery({ queryKey: ["health"], queryFn: fetchHealth, staleTime: Infinity });
|
||||
}
|
||||
@@ -20,7 +20,11 @@ import { useSyncExternalStore } from "react";
|
||||
*/
|
||||
const SESSION_KEY = "aurum:basicAuth";
|
||||
const REMEMBER_KEY = "aurum:basicAuth:remember";
|
||||
const REMEMBER_DAYS = 30;
|
||||
// 7, not 30: what's stored is the Basic Auth header, i.e. the instance
|
||||
// password recoverable in plaintext by anyone who can read localStorage (an
|
||||
// XSS, a browser extension, someone with the machine). It can't be revoked
|
||||
// server-side, so the only lever on that exposure is how long it sits there.
|
||||
const REMEMBER_DAYS = 7;
|
||||
const REMEMBER_MS = REMEMBER_DAYS * 24 * 60 * 60 * 1000;
|
||||
|
||||
interface RememberedEntry {
|
||||
|
||||
@@ -49,7 +49,7 @@ const ru = {
|
||||
"auth.subtitle": "Личные финансы",
|
||||
"auth.usernameLabel": "Логин",
|
||||
"auth.passwordLabel": "Пароль",
|
||||
"auth.rememberMe": "Запомнить меня на 30 дней",
|
||||
"auth.rememberMe": "Запомнить меня на 7 дней",
|
||||
"auth.submitButton": "Войти",
|
||||
"auth.submitting": "Вход…",
|
||||
"auth.errorInvalidCredentials": "Неверный логин или пароль",
|
||||
@@ -567,7 +567,7 @@ const en: Record<keyof typeof ru, string> = {
|
||||
"auth.subtitle": "Personal finance",
|
||||
"auth.usernameLabel": "Username",
|
||||
"auth.passwordLabel": "Password",
|
||||
"auth.rememberMe": "Remember me for 30 days",
|
||||
"auth.rememberMe": "Remember me for 7 days",
|
||||
"auth.submitButton": "Sign in",
|
||||
"auth.submitting": "Signing in…",
|
||||
"auth.errorInvalidCredentials": "Incorrect username or password",
|
||||
|
||||
@@ -2,14 +2,14 @@ import { AlertThresholdsCard } from "@/components/settings/AlertThresholdsCard";
|
||||
import { BackupCard } from "@/components/settings/BackupCard";
|
||||
import { CurrencyCard } from "@/components/settings/CurrencyCard";
|
||||
import { PreferencesCard } from "@/components/settings/PreferencesCard";
|
||||
import { useHealth } from "@/hooks/useHealth";
|
||||
import { useAppSettings } from "@/hooks/useSettings";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export function SettingsPage() {
|
||||
// Purely informational — if /api/health hasn't answered yet (or is
|
||||
// Purely informational — if the settings request hasn't answered yet (or is
|
||||
// unreachable), just show nothing rather than a loading/error state for
|
||||
// one line of fine print.
|
||||
const { data: health } = useHealth();
|
||||
const { data: appSettings } = useAppSettings();
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -17,9 +17,9 @@ export function SettingsPage() {
|
||||
<CurrencyCard />
|
||||
<AlertThresholdsCard />
|
||||
<BackupCard />
|
||||
{health?.version && (
|
||||
{appSettings?.app_version && (
|
||||
<p className="text-center text-xs text-text-muted">
|
||||
{t("settings.version", { version: health.version })}
|
||||
{t("settings.version", { version: appSettings.app_version })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -565,10 +565,8 @@ export interface AppSettings {
|
||||
risky_allocation_threshold_percent: number;
|
||||
idle_cash_threshold_amount: string;
|
||||
idle_cash_threshold_days: number;
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
status: string;
|
||||
version: string;
|
||||
/** Read-only: the running backend release. Comes back here rather than
|
||||
* from /api/health, which is served without auth. */
|
||||
app_version: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user