fix: deploy crash from MutableHeaders.pop + missing wget
Two issues blocked the security-fix deploy: 1. Starlette's MutableHeaders has no .pop() — del-via-membership is the correct mapping op. The previous code crashed every request with AttributeError, taking the container's healthcheck down with it. 2. Coolify's in-container healthcheck falls back to wget which wasn't installed in our slim base. Add wget alongside curl. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,10 @@
|
||||
"details": "File: nadlan_mcp/web_app.py — proxy_appraisal_pdf. Validate parsed.username is None, parsed.password is None, and parsed.netloc == parsed.hostname (no userinfo, no port). Add unit test with malicious URL.",
|
||||
"testStrategy": "Unit test: pass crafted userinfo URL, expect 400. Verify whitelisted URLs still work.",
|
||||
"priority": "high",
|
||||
"status": "in-progress",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:42:36.172Z"
|
||||
"updatedAt": "2026-04-25T17:59:18.410Z"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
@@ -20,9 +20,10 @@
|
||||
"details": "Files: nadlan_mcp/web_app.py — search_address, search_deals, search_appraisals, proxy_appraisal_pdf. Use logger.exception(...) for full detail; return generic detail='Service temporarily unavailable' to clients.",
|
||||
"testStrategy": "Trigger an upstream failure (mock), verify response detail is generic and full error is in logs.",
|
||||
"priority": "high",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.428Z"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
@@ -31,9 +32,10 @@
|
||||
"details": "File: nadlan_mcp/web_app.py — FastAPI(...). Use ENVIRONMENT env var; set docs_url=None and openapi_url=None when production.",
|
||||
"testStrategy": "GET /api/docs in prod → 404. In dev → 200.",
|
||||
"priority": "high",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.445Z"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
@@ -42,11 +44,12 @@
|
||||
"details": "File: nadlan_mcp/web_app.py — proxy_appraisal_pdf. Use iter_content(chunk_size=8192). Check Content-Length header → reject >50MB. Add 5-min total timeout.",
|
||||
"testStrategy": "Mock upstream with 100MB body → expect 413. With 1MB body → 200, streamed.",
|
||||
"priority": "high",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"1"
|
||||
],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.475Z"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
@@ -55,9 +58,10 @@
|
||||
"details": "Apply Traefik mtls-forwardauth@file middleware (existing infrastructure under mtls-auth-service). Coolify custom_labels need mtls-optional@file TLS option + middleware. /mcp may need separate API-key gate. Public health endpoint must remain reachable for Coolify probe.",
|
||||
"testStrategy": "Without client cert → 403. With valid cert from PKI → 200. /api/health remains 200 anonymously.",
|
||||
"priority": "high",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.490Z"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
@@ -66,9 +70,10 @@
|
||||
"details": "File: nadlan_mcp/web_app.py — add middleware setting all standard headers. CSP needs to allow Google Fonts (until L-1 self-hosts) or self only.",
|
||||
"testStrategy": "curl -sI / → expect all 6 headers present with sane values.",
|
||||
"priority": "high",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.602Z"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
@@ -77,9 +82,10 @@
|
||||
"details": "Use slowapi (FastAPI rate-limiter) or Traefik RateLimit middleware. Suggested: 60 req/min per IP on /api/search/*, 30 req/min on /mcp.",
|
||||
"testStrategy": "Hammer 100 reqs/sec from single IP → expect 429 after limit.",
|
||||
"priority": "high",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.624Z"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
@@ -88,9 +94,10 @@
|
||||
"details": "File: nadlan_mcp/web_app.py. Set allow_headers=['Content-Type'], allow_methods=['GET','POST','OPTIONS'], allow_origins from ALLOWED_ORIGINS env var.",
|
||||
"testStrategy": "Origin header from disallowed source → no Access-Control-Allow-Origin in response.",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.635Z"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
@@ -99,9 +106,10 @@
|
||||
"details": "File: nadlan_mcp/govmap/models.py. Add explicit fields for gushNum, parcelNum, subParcelNum, streetNameHeb, settlementNameHeb, houseNum (currently extras). Switch to extra='ignore' (forbid is too strict — Govmap adds fields). Update tests.",
|
||||
"testStrategy": "Existing tests pass; new fields appear in serialized output; unknown upstream fields are ignored, not propagated.",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.645Z"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
@@ -110,9 +118,10 @@
|
||||
"details": "Files: nadlan_mcp/web_app.py (DealsSearchRequest, AppraisalsSearchRequest). Add @field_validator: block ^\\d{1,8}$, plot ^\\d{1,6}$, decisive_appraiser Hebrew/space/dash, address Hebrew/Latin/digits/punctuation.",
|
||||
"testStrategy": "POST with null bytes / SQL-like payloads → 422. Real Hebrew searches still pass.",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.671Z"
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
@@ -121,9 +130,10 @@
|
||||
"details": "Add Starlette middleware to reject Content-Length > 1MB on /api/* endpoints (none of our endpoints need large bodies).",
|
||||
"testStrategy": "POST 10MB body → 413 before parsing.",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.679Z"
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
@@ -132,9 +142,10 @@
|
||||
"details": "Edit .gitea/workflows/deploy.yaml — replace literal with ${{ secrets.COOLIFY_UUID }}. Add secret in Gitea Actions settings.",
|
||||
"testStrategy": "Trigger workflow, verify deploy still works.",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.686Z"
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
@@ -143,9 +154,10 @@
|
||||
"details": "Resolve current images to digests via docker manifest inspect. Pin gcc/curl in Dockerfile.",
|
||||
"testStrategy": "Rebuild → identical digest. CVE scanner reports specific versions.",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.698Z"
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
@@ -154,9 +166,10 @@
|
||||
"details": "pip-compile or pip freeze → requirements.lock committed. Dockerfile uses pip install -r requirements.lock instead of -e .",
|
||||
"testStrategy": "Two consecutive builds → identical pip freeze.",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.712Z"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
@@ -165,9 +178,10 @@
|
||||
"details": "Download Heebo woff2 files into web/public/fonts/. Update web/index.html link tags + add @font-face in CSS.",
|
||||
"testStrategy": "Network tab → no requests to fonts.googleapis.com.",
|
||||
"priority": "low",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.717Z"
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
@@ -176,9 +190,10 @@
|
||||
"details": "Middleware: /assets/* → Cache-Control: public, max-age=31536000, immutable; index.html → no-cache, no-store.",
|
||||
"testStrategy": "curl -I /assets/index-xxx.js → expect immutable.",
|
||||
"priority": "low",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.725Z"
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
@@ -187,9 +202,10 @@
|
||||
"details": "Files: nadlan_mcp/govmap/client.py, nadlan_mcp/govil/client.py. Replace f-strings that include user input with sanitized versions; only log lengths or hashes.",
|
||||
"testStrategy": "Search logs for sample query → not present.",
|
||||
"priority": "low",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.733Z"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
@@ -198,9 +214,10 @@
|
||||
"details": "Replace with literal 8000 — that's our fixed contract.",
|
||||
"testStrategy": "docker inspect → HEALTHCHECK uses literal 8000.",
|
||||
"priority": "low",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.751Z"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
@@ -209,18 +226,19 @@
|
||||
"details": "Wrap iter_content with asyncio.timeout(300) (5 min hard cap).",
|
||||
"testStrategy": "Mock upstream that sends one chunk then sleeps → connection closed at 5 min.",
|
||||
"priority": "low",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"4"
|
||||
],
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T17:59:18.760Z"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2026-04-25T17:42:36.190Z",
|
||||
"lastModified": "2026-04-25T17:59:18.761Z",
|
||||
"taskCount": 19,
|
||||
"completedCount": 0,
|
||||
"completedCount": 19,
|
||||
"tags": [
|
||||
"master"
|
||||
]
|
||||
|
||||
+2
-2
@@ -22,9 +22,9 @@ ENV PYTHONUNBUFFERED=1 \
|
||||
WORKDIR /app
|
||||
|
||||
# gcc — for any pure-Python wheels without prebuilt slim wheels.
|
||||
# curl — Coolify runs its healthcheck via curl inside the container.
|
||||
# curl + wget — Coolify falls back to wget for its in-container healthcheck.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends gcc curl \
|
||||
&& apt-get install -y --no-install-recommends gcc curl wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python deps from pyproject.
|
||||
|
||||
@@ -338,8 +338,10 @@ def create_app() -> FastAPI:
|
||||
response.headers.setdefault(
|
||||
"Strict-Transport-Security", "max-age=31536000; includeSubDomains"
|
||||
)
|
||||
# Drop the server-version banner.
|
||||
response.headers.pop("server", None)
|
||||
# Drop the server-version banner — MutableHeaders has no .pop(),
|
||||
# use the mapping protocol instead.
|
||||
if "server" in response.headers:
|
||||
del response.headers["server"]
|
||||
# Cache hashed asset bundles aggressively (Vite already adds a hash
|
||||
# to filenames so cache invalidation is a build problem, not a
|
||||
# runtime one). Everything else: don't cache.
|
||||
|
||||
Reference in New Issue
Block a user