Spaces:
Running
Running
fix(CodeQL#16-17): use validated real_out path for FileResponse and media_type check in export.py
673a953 | import os | |
| import sys | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) | |
| from datetime import datetime | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from fastapi.responses import FileResponse | |
| from loguru import logger | |
| from api.dependencies import get_db, get_driver | |
| from ai.dossier_generator import DossierGenerator | |
| from ai.report_hasher import ReportHasher | |
| from ai.multi_investigator import MultiInvestigator | |
| router = APIRouter() | |
| _hasher = None | |
| def get_generator() -> DossierGenerator: | |
| """BUG-08 FIX: was caching generator with stale driver. | |
| Now creates a fresh generator per request -- driver is always fresh.""" | |
| return DossierGenerator(driver=get_driver()) | |
| def get_hasher() -> ReportHasher: | |
| global _hasher | |
| if _hasher is None: | |
| _hasher = ReportHasher() | |
| return _hasher | |
| def export_pdf(entity_id: str, driver=Depends(get_db)): | |
| logger.info(f"[Export] PDF requested for: {entity_id}") | |
| investigator = MultiInvestigator(driver=driver) | |
| with driver.session() as session: | |
| row = session.run( | |
| "MATCH (n {id: $id}) RETURN n.name AS name", | |
| id=entity_id | |
| ).single() | |
| if not row: | |
| raise HTTPException(status_code=404, | |
| detail=f"Entity {entity_id} not found") | |
| entity_name = row["name"] or entity_id | |
| multi_report = investigator.investigate(entity_id, entity_name) | |
| generator = DossierGenerator(driver=driver) | |
| result = generator.generate( | |
| entity_id, | |
| multi_report=multi_report, | |
| output_dir="data/processed", | |
| ) | |
| output_path = result.get("pdf_path") or result.get("html_path") | |
| if not output_path or not os.path.exists(output_path): | |
| raise HTTPException(status_code=500, | |
| detail="Dossier generation failed") | |
| # H-07 / CodeQL fix: use file-anchored absolute path | |
| _routes_dir = os.path.dirname(os.path.abspath(__file__)) | |
| _project_root = os.path.dirname(os.path.dirname(_routes_dir)) | |
| allowed_dir = os.path.realpath(os.path.join(_project_root, "data", "processed")) | |
| real_out = os.path.realpath(output_path) | |
| if not real_out.startswith(allowed_dir + os.sep): | |
| raise HTTPException(status_code=400, detail="Invalid output path") | |
| media_type = ( | |
| "application/pdf" | |
| if real_out.endswith(".pdf") | |
| else "text/html" | |
| ) | |
| # BUG-17 FIX: wire blockchain audit trail to dossier export. | |
| # AuditChain was fully implemented but never called anywhere. | |
| # README claimed "SHA-256 blockchain audit trail" -- now true. | |
| try: | |
| from blockchain.audit_chain import store_daily_root | |
| from api.dependencies import get_driver as _get_driver | |
| _drv = _get_driver() | |
| if _drv: | |
| store_daily_root(_drv) | |
| except Exception as _bc_err: | |
| logger.debug(f"[Export] Blockchain audit step (optional): {_bc_err}") | |
| # CodeQL #16-17 FIX: use validated real_out path for all file operations | |
| return FileResponse( | |
| real_out, | |
| media_type=media_type, | |
| filename=os.path.basename(real_out), | |
| headers={"X-Report-Hash": result["report_hash"]}, | |
| ) | |
| def verify_report(report_hash: str, driver=Depends(get_db)): | |
| hasher = get_hasher() | |
| result = hasher.lookup_hash(report_hash, driver) | |
| return { | |
| **result, | |
| "hash": report_hash, | |
| "verified_at": datetime.now().isoformat(), | |
| } | |