fix(tradein): scope GET /estimate/{id} + /pdf to owner, close IDOR (#690) #702
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#702
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/690-idor-estimate-scope"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
GET /estimate/{id}и/estimate/{id}/pdfтеперь скоупятся по владельцу.client_name/client_phone)._assert_estimate_access()— зеркало скоупинга/history(#656): владелец (created_by == X-Authenticated-User) или admin (get_role); иначе 404, 401 без заголовка, 403 если юзер внеroles.yaml.Notes
ruff-formatпереформатировал весь файл — pinned hook rev (0.7.4) дрейфанул от закоммиченного main. Семантические правки локальны: helper + header-param +created_byв двух SELECT + вызовы guard.Test plan
uv run pytest tests/test_estimate_idor.py tests/test_history_scope.py→ 14 passed (owner 200 / чужой pilot 404 / admin 200 / без заголовка 401 / неизвестный юзер 403, для GET и PDF).Closes #690
Любой авторизованный pilot мог читать/скачивать чужую оценку (включая client_name/client_phone) по UUID — GET /estimate/{id} и /estimate/{id}/pdf не проверяли владельца. Добавлен guard _assert_estimate_access(): зеркало скоупинга /history (#656). Эндпоинты тянут created_by из trade_in_estimates и пропускают только владельца (created_by == X-Authenticated-User) или admin (get_role); иначе 404 (скрываем существование), 401 без заголовка, 403 если юзер вне roles.yaml. ruff-format переформатировал весь файл (rev-дрейф pinned 0.7.4) — семантика правок локальна: helper + header-param + created_by в двух SELECT. Closes #690Verdict: FIX (changes needed before merge). The guard you added is correct and fail-safe —
_assert_estimate_accesshandles 401 (no header) / 403 (not in roles, KeyError) / admin-bypass / 404 (pilot-reads-other AND not-found, so existence isn't leaked) / legacy-NULL created_by → 404 for non-admin. The two named endpoints (GET /estimate/{id}+/pdf) are properly closed, owner is fetched before any data is returned, SQL is parameterized (CAST(:id AS uuid)), and the −112 lines are pure reformatting (no functionality dropped).But the PR title says "close IDOR (#690)" and the IDOR is NOT fully closed — 10 other routes that read by
estimate_idare still unguarded. Most critical:1. MUST FIX — photo endpoints leak interior photos (highest PII, enumerable):
GET /estimate/{id}/photos(≈L412) — lists photo metadata/ids for any estimate id.GET /estimate/{id}/photos/{photo_id}(≈L436) — returns raw apartment-interior photo bytes, scoped only by(photo_id, estimate_id), no owner check. An attacker who knows/guesses an estimate UUID enumerates/photos→ downloads every interior photo of another pilot's client. This is the same IDOR class as #690, worse data. Apply_assert_estimate_access(fetch the estimate'screated_by, then guard) to both before returning anything.2. SHOULD FIX (or explicitly de-scope into a tracked follow-up issue):
POST /estimate/{id}/photos(≈L337) — write-side IDOR: any pilot can attach photos to another pilot's estimate./houses(L565),/placement-history(L652),/house-analytics(L734),/cian-price-changes(L924),/sell-time-sensitivity(L1013),/imv-benchmark(L1194). Lower severity (address only, no client_name/phone) but still cross-pilot existence+address confirmation.The
_assert_estimate_accesshelper already exists — extending it to these is mechanical (fetchcreated_byfrom the estimate row, call the guard). Either guard them in this PR, or if #690 is scoped to just the 2 routes, say so in the PR body and open a tracked follow-up issue for the photo IDOR (it should NOT sit open silently — it's a live PII leak).3. Test gaps to add: no
legacy NULL created_bytest (guard handles it, untested); nounknown-user → 403test for the/pdfroute (only the JSON route has it).Re-request review after the fix; I'll re-review at the new head SHA.
Адресовал все находки ревью в новом коммите
bf55b4b:MUST FIX — закрыто:
GET /photos,GET /photos/{photo_id}(сырые байты фото интерьера),POST /photos(write-side) теперь под guard.SHOULD FIX — закрыто в этом же PR (не пунчу в follow-up): все address-leaking derived-роуты —
/houses,/placement-history,/house-analytics,/cian-price-changes,/sell-time-sensitivity,/imv-benchmark.Механика: новый
_assert_estimate_access_by_id(db, estimate_id, user)тянетcreated_byпоestimate_idи делегирует существующему_assert_estimate_access.upload_photoпереиспользует existence-SELECT (SELECT 1→SELECT created_by, без лишнего запроса).Test gaps — закрыты: legacy NULL
created_by→ 404 (non-admin); unknown-user → 403 на/pdf; + owner/other-pilot/admin/no-header на photo-роутах, write-side 404, derived houses+imv.pytest tests/test_estimate_idor.py tests/test_history_scope.py→ 27 passed;ruff@0.7.4 checkclean.Теперь все роуты, читающие по
estimate_id, скоупятся по владельцу. Прошу ре-ревью на headbf55b4b.Re-review after fix. IDOR now FULLY closed across all by-estimate_id routes. New helper _assert_estimate_access_by_id (fetch created_by, 404-if-missing so existence not confirmed, parameterized CAST(:id AS uuid), applies _assert_estimate_access) is wired into all previously-unguarded routes: list_photos + get_photo (the enumerable interior-photo PII leak), upload_photo (write-IDOR), houses, placement-history, house-analytics, cian-price-changes, sell-time-sensitivity, imv-benchmark. Original GET /estimate/{id} + /pdf still guarded. Verified every by-id route handler now has a guard call; +220 lines of IDOR tests added. 0 conflicts. Fully addresses the prior fix-list. APPROVE.