security: address all findings from vulnerability assessment
Implements the fixes catalogued under Task Master tasks 1-19, derived from the security audit at .claude/plans/adaptive-sleeping-diffie.md. Critical - C-1 SSRF userinfo bypass: PDF proxy now rejects URLs containing userinfo or non-default ports, and asserts netloc == hostname. - C-2 No authentication: optional NADLAN_API_KEY gate guards /api/search/*, /api/appraisals/pdf and /mcp until Traefik mTLS is wired up. Default behavior unchanged when env unset. High - H-1 Error info leak: HTTPException details replaced with generic string; full exceptions still logged via logger.exception. - H-2 Security headers middleware: HSTS, X-Frame-Options, nosniff, Referrer-Policy, Permissions-Policy, CSP, server-banner stripped. - H-3 Swagger gated: /api/docs and /api/openapi.json hidden when ENVIRONMENT=production. - H-4 PDF proxy DoS: streams in 8KB chunks, rejects upstream above 50MB Content-Length, also rejects mid-stream overrun. Magic-byte check moved before StreamingResponse so we can 502 cleanly. - H-5 Per-IP rate limit: token-bucket per (path, IP) on /mcp, /api/search/*, /api/appraisals/pdf. Reads X-Forwarded-For for client IP behind Traefik. Medium - M-1 CORS: ALLOWED_ORIGINS env, allow_headers narrowed, no creds. - M-2 Deal model: extra="ignore" + explicit fields for gushNum, parcelNum, subParcelNum, streetNameHeb, houseNum, deal_source, deal_type, deal_type_description, distance_meters. Frontend now reads snake_case. - M-3 Input validators: regex on block, plot, appraiser names, dates, address, free-text — Hebrew + Latin + safe punctuation. - M-4 COOLIFY_UUID moved out of deploy.yaml into Gitea Actions secret. - M-5 Base images pinned to sha256 digests. - M-6 Runtime deps tightened to ~= compatible-release. - M-7 1MB request-body size limit middleware. Low / privacy - L-1 Heebo self-hosted via @fontsource. Removed Google Fonts links and CSP entries. Added meta referrer + meta robots noindex. - L-2 Cache-Control: immutable for /assets, no-cache for HTML. - L-3 Logger no longer prints user-supplied addresses; emits length only. - L-4 Dockerfile healthcheck uses literal port 8000. - L-5 PDF proxy stream cleanup uses contextlib.suppress. Tests: 331 passed, 17 skipped. Frontend typecheck passes. Findings file: .claude/plans/adaptive-sleeping-diffie.md Tasks: .taskmaster/tasks/tasks.json Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-7
@@ -3,14 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>נדל"ן ושמאי מכריע — Marcus-Law</title>
|
||||
<title>נדל"ן ושמאי מכריע</title>
|
||||
<meta name="description" content="חיפוש עסקאות נדל"ן והחלטות שמאי מכריע באזור ספציפי" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Heebo:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
</head>
|
||||
<body dir="rtl">
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+10
@@ -8,6 +8,7 @@
|
||||
"name": "nadlan-mcp-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fontsource/heebo": "^5.2.8",
|
||||
"@radix-ui/react-direction": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
@@ -771,6 +772,15 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/heebo": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/heebo/-/heebo-5.2.8.tgz",
|
||||
"integrity": "sha512-K5aMLH2Yy/CmmjuM9lpToyIEwSdPKwW6KzZOGCFnj7XFrgatIpnesu6FDgmQl3bkyP/YCT62rzLRqRM9s/OzLw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"lint": "eslint . --ext ts,tsx --max-warnings=0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/heebo": "^5.2.8",
|
||||
"@radix-ui/react-direction": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
|
||||
@@ -36,12 +36,12 @@ export interface Deal {
|
||||
floor?: number;
|
||||
deal_date?: string;
|
||||
property_type_description?: string;
|
||||
settlementNameHeb?: string;
|
||||
streetNameHeb?: string;
|
||||
houseNum?: string | number;
|
||||
gushNum?: number | null;
|
||||
parcelNum?: number | null;
|
||||
subParcelNum?: number | null;
|
||||
settlement_name_heb?: string;
|
||||
street_name_heb?: string;
|
||||
house_num?: string | number;
|
||||
gush_num?: number | null;
|
||||
parcel_num?: number | null;
|
||||
sub_parcel_num?: number | null;
|
||||
price_per_sqm?: number;
|
||||
deal_source?: string | null;
|
||||
[key: string]: unknown;
|
||||
|
||||
@@ -37,11 +37,11 @@ function streetByClosestParcel(
|
||||
const blockNum = block ? parseInt(block, 10) : null;
|
||||
let best: { dist: number; street: string } | null = null;
|
||||
for (const d of deals) {
|
||||
if (!d.streetNameHeb || d.parcelNum == null) continue;
|
||||
if (blockNum !== null && d.gushNum !== blockNum) continue;
|
||||
const dist = Math.abs((d.parcelNum as number) - target);
|
||||
if (!d.street_name_heb || d.parcel_num == null) continue;
|
||||
if (blockNum !== null && d.gush_num !== blockNum) continue;
|
||||
const dist = Math.abs((d.parcel_num as number) - target);
|
||||
if (!best || dist < best.dist) {
|
||||
best = { dist, street: d.streetNameHeb };
|
||||
best = { dist, street: d.street_name_heb };
|
||||
}
|
||||
}
|
||||
return best?.street ?? null;
|
||||
@@ -52,13 +52,13 @@ function proximityLabel(deal: Deal, inferredStreet: string | null): string {
|
||||
if (deal.deal_source === "same_building") return "אותו בניין";
|
||||
if (deal.deal_source === "neighborhood") return "שכונה";
|
||||
// "street" source — check if actually same street
|
||||
if (inferredStreet && deal.streetNameHeb === inferredStreet) return "אותו רחוב";
|
||||
if (inferredStreet && deal.street_name_heb === inferredStreet) return "אותו רחוב";
|
||||
return "סביבה קרובה";
|
||||
}
|
||||
|
||||
function proximityClass(deal: Deal, inferredStreet: string | null): string {
|
||||
if (deal.deal_source === "same_building") return "bg-emerald-100 text-emerald-800";
|
||||
if (deal.deal_source === "street" && inferredStreet && deal.streetNameHeb === inferredStreet)
|
||||
if (deal.deal_source === "street" && inferredStreet && deal.street_name_heb === inferredStreet)
|
||||
return "bg-blue-100 text-blue-800";
|
||||
if (deal.deal_source === "street") return "bg-sky-50 text-sky-700";
|
||||
return "bg-slate-100 text-slate-700";
|
||||
@@ -100,9 +100,9 @@ export function DealsTable({ deals, resolvedAddress, searchedBlock, searchedPlot
|
||||
</thead>
|
||||
<tbody>
|
||||
{deals.map((d) => {
|
||||
const street = d.streetNameHeb || "";
|
||||
const houseNum = d.houseNum ?? "";
|
||||
const settlement = d.settlementNameHeb || "";
|
||||
const street = d.street_name_heb || "";
|
||||
const houseNum = d.house_num ?? "";
|
||||
const settlement = d.settlement_name_heb || "";
|
||||
const address = [street, houseNum, settlement].filter(Boolean).join(" ");
|
||||
return (
|
||||
<tr key={d.id} className="border-t hover:bg-accent/40">
|
||||
@@ -118,9 +118,9 @@ export function DealsTable({ deals, resolvedAddress, searchedBlock, searchedPlot
|
||||
<td className="p-3 text-center tabular-nums">
|
||||
{formatCurrencyILS(d.price_per_sqm)}
|
||||
</td>
|
||||
<td className="p-3 text-center tabular-nums">{d.gushNum ?? "—"}</td>
|
||||
<td className="p-3 text-center tabular-nums">{d.parcelNum ?? "—"}</td>
|
||||
<td className="p-3 text-center tabular-nums">{d.subParcelNum ?? "—"}</td>
|
||||
<td className="p-3 text-center tabular-nums">{d.gush_num ?? "—"}</td>
|
||||
<td className="p-3 text-center tabular-nums">{d.parcel_num ?? "—"}</td>
|
||||
<td className="p-3 text-center tabular-nums">{d.sub_parcel_num ?? "—"}</td>
|
||||
<td className="p-3">
|
||||
{d.deal_source ? (
|
||||
<span
|
||||
|
||||
@@ -3,6 +3,13 @@ import ReactDOM from "react-dom/client";
|
||||
import { DirectionProvider } from "@radix-ui/react-direction";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import App from "./App";
|
||||
// Self-host the Heebo font (privacy: avoids leaking the visited URL to
|
||||
// Google Fonts via the Referer header — see security audit L-1).
|
||||
import "@fontsource/heebo/300.css";
|
||||
import "@fontsource/heebo/400.css";
|
||||
import "@fontsource/heebo/500.css";
|
||||
import "@fontsource/heebo/600.css";
|
||||
import "@fontsource/heebo/700.css";
|
||||
import "./index.css";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
||||
Reference in New Issue
Block a user