Spaces:
Sleeping
Sleeping
Commit ·
d35c03c
1
Parent(s): f873b45
fix: path traversal in model endpoints, XSS in plan card, remove dead code
Browse files- Validate model file names against safe character regex and resolve path
to prevent directory traversal in all /api/models/ endpoints
- Escape HTML in plan card and wizard review to prevent XSS from agent
responses containing markup
- Remove duplicate _is_plan_trigger check in _run_crew (dead code, already
handled in chat_turn)
- Use Literal type for DesignState.phase field to catch invalid states
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- agents/crew_orchestrator.py +0 -12
- agents/design_state.py +3 -1
- server/web.py +23 -8
- web/index.html +3 -3
agents/crew_orchestrator.py
CHANGED
|
@@ -160,18 +160,6 @@ class CrewOrchestrator(BaseOrchestrator):
|
|
| 160 |
|
| 161 |
state = DesignState(**(design_state_dict or {}))
|
| 162 |
|
| 163 |
-
# Phase: manual plan trigger
|
| 164 |
-
if state.phase == "exploring" and _is_plan_trigger(message):
|
| 165 |
-
score = compute_score(state)
|
| 166 |
-
plan = DesignPlan.from_state(state, confidence_score=score)
|
| 167 |
-
state.phase = "planning"
|
| 168 |
-
state.plan = plan
|
| 169 |
-
return {
|
| 170 |
-
"responses": [],
|
| 171 |
-
"preview": None,
|
| 172 |
-
"design_state": state.model_dump(),
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
# Phase: if in planning and user sends a message, reset to exploring
|
| 176 |
if state.phase == "planning":
|
| 177 |
state.phase = "exploring"
|
|
|
|
| 160 |
|
| 161 |
state = DesignState(**(design_state_dict or {}))
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
# Phase: if in planning and user sends a message, reset to exploring
|
| 164 |
if state.phase == "planning":
|
| 165 |
state.phase = "exploring"
|
agents/design_state.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import re
|
|
|
|
|
|
|
| 6 |
from pydantic import BaseModel, Field
|
| 7 |
|
| 8 |
from config.settings import settings
|
|
@@ -100,7 +102,7 @@ class DesignState(BaseModel):
|
|
| 100 |
constraints: list[str] = Field(default_factory=list)
|
| 101 |
decisions: list[str] = Field(default_factory=list)
|
| 102 |
axis_recommendation: str = ""
|
| 103 |
-
phase:
|
| 104 |
plan: DesignPlan | None = None
|
| 105 |
|
| 106 |
def render(self) -> str:
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import re
|
| 6 |
+
from typing import Literal
|
| 7 |
+
|
| 8 |
from pydantic import BaseModel, Field
|
| 9 |
|
| 10 |
from config.settings import settings
|
|
|
|
| 102 |
constraints: list[str] = Field(default_factory=list)
|
| 103 |
decisions: list[str] = Field(default_factory=list)
|
| 104 |
axis_recommendation: str = ""
|
| 105 |
+
phase: Literal["exploring", "planning", "approved"] = "exploring"
|
| 106 |
plan: DesignPlan | None = None
|
| 107 |
|
| 108 |
def render(self) -> str:
|
server/web.py
CHANGED
|
@@ -181,34 +181,49 @@ async def list_models():
|
|
| 181 |
return JSONResponse(result)
|
| 182 |
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
@app.get("/api/models/{name}.stl")
|
| 185 |
async def get_stl(name: str):
|
| 186 |
-
path =
|
| 187 |
-
if not path.exists():
|
| 188 |
return JSONResponse({"error": f"STL not found: {name}"}, status_code=404)
|
| 189 |
return FileResponse(path, media_type="model/stl", filename=f"{name}.stl")
|
| 190 |
|
| 191 |
|
| 192 |
@app.get("/api/models/{name}.step")
|
| 193 |
async def get_step(name: str):
|
| 194 |
-
path =
|
| 195 |
-
if not path.exists():
|
| 196 |
return JSONResponse({"error": f"STEP not found: {name}"}, status_code=404)
|
| 197 |
return FileResponse(path, media_type="application/step", filename=f"{name}.step")
|
| 198 |
|
| 199 |
|
| 200 |
@app.get("/api/models/{name}.gcode")
|
| 201 |
async def get_gcode(name: str):
|
| 202 |
-
path =
|
| 203 |
-
if not path.exists():
|
| 204 |
return JSONResponse({"error": f"G-code not found: {name}"}, status_code=404)
|
| 205 |
return FileResponse(path, media_type="text/plain", filename=f"{name}.gcode")
|
| 206 |
|
| 207 |
|
| 208 |
@app.get("/api/models/{name}.3mf")
|
| 209 |
async def get_3mf(name: str):
|
| 210 |
-
path =
|
| 211 |
-
if not path.exists():
|
| 212 |
return JSONResponse({"error": f"3MF not found: {name}"}, status_code=404)
|
| 213 |
return FileResponse(path, media_type="model/3mf", filename=f"{name}.3mf")
|
| 214 |
|
|
|
|
| 181 |
return JSONResponse(result)
|
| 182 |
|
| 183 |
|
| 184 |
+
import re
|
| 185 |
+
|
| 186 |
+
_SAFE_NAME = re.compile(r'^[a-zA-Z0-9_\-]+$')
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _safe_model_path(name: str, ext: str) -> Path | None:
|
| 190 |
+
"""Validate model name and return safe path, or None if invalid."""
|
| 191 |
+
if not _SAFE_NAME.match(name):
|
| 192 |
+
return None
|
| 193 |
+
path = (OUTPUT_DIR / f"{name}.{ext}").resolve()
|
| 194 |
+
if not str(path).startswith(str(OUTPUT_DIR.resolve())):
|
| 195 |
+
return None
|
| 196 |
+
return path
|
| 197 |
+
|
| 198 |
+
|
| 199 |
@app.get("/api/models/{name}.stl")
|
| 200 |
async def get_stl(name: str):
|
| 201 |
+
path = _safe_model_path(name, "stl")
|
| 202 |
+
if not path or not path.exists():
|
| 203 |
return JSONResponse({"error": f"STL not found: {name}"}, status_code=404)
|
| 204 |
return FileResponse(path, media_type="model/stl", filename=f"{name}.stl")
|
| 205 |
|
| 206 |
|
| 207 |
@app.get("/api/models/{name}.step")
|
| 208 |
async def get_step(name: str):
|
| 209 |
+
path = _safe_model_path(name, "step")
|
| 210 |
+
if not path or not path.exists():
|
| 211 |
return JSONResponse({"error": f"STEP not found: {name}"}, status_code=404)
|
| 212 |
return FileResponse(path, media_type="application/step", filename=f"{name}.step")
|
| 213 |
|
| 214 |
|
| 215 |
@app.get("/api/models/{name}.gcode")
|
| 216 |
async def get_gcode(name: str):
|
| 217 |
+
path = _safe_model_path(name, "gcode")
|
| 218 |
+
if not path or not path.exists():
|
| 219 |
return JSONResponse({"error": f"G-code not found: {name}"}, status_code=404)
|
| 220 |
return FileResponse(path, media_type="text/plain", filename=f"{name}.gcode")
|
| 221 |
|
| 222 |
|
| 223 |
@app.get("/api/models/{name}.3mf")
|
| 224 |
async def get_3mf(name: str):
|
| 225 |
+
path = _safe_model_path(name, "3mf")
|
| 226 |
+
if not path or not path.exists():
|
| 227 |
return JSONResponse({"error": f"3MF not found: {name}"}, status_code=404)
|
| 228 |
return FileResponse(path, media_type="model/3mf", filename=f"{name}.3mf")
|
| 229 |
|
web/index.html
CHANGED
|
@@ -1723,7 +1723,7 @@ function wizUpdateReview() {
|
|
| 1723 |
];
|
| 1724 |
let html = '';
|
| 1725 |
for (const [label, value] of fields) {
|
| 1726 |
-
html += '<div class="wizard-review-field"><span class="wizard-review-label">' + label + '</span><span class="wizard-review-value">' + (value || '\u2014') + '</span></div>';
|
| 1727 |
}
|
| 1728 |
el.innerHTML = html;
|
| 1729 |
const score = wizComputeScore();
|
|
@@ -1800,10 +1800,10 @@ function renderPlanCard(plan) {
|
|
| 1800 |
let html = '<div class="plan-card" id="active-plan-card">';
|
| 1801 |
html += '<div class="plan-card-title">\u25c6 PLAN READY FOR REVIEW</div>';
|
| 1802 |
for (const [label, value] of fields) {
|
| 1803 |
-
html += '<div class="wizard-review-field"><span class="wizard-review-label">' + label + '</span><span class="wizard-review-value">' + (value || '\u2014') + '</span></div>';
|
| 1804 |
}
|
| 1805 |
if (plan.machining_notes && plan.machining_notes.length) {
|
| 1806 |
-
html += '<div class="wizard-review-field"><span class="wizard-review-label">Notes</span><span class="wizard-review-value">' + plan.machining_notes.join('; ') + '</span></div>';
|
| 1807 |
}
|
| 1808 |
html += '<div class="plan-card-score">Score: ' + (plan.confidence_score || 0).toFixed(0) + '/8</div>';
|
| 1809 |
html += '<div class="plan-card-actions">';
|
|
|
|
| 1723 |
];
|
| 1724 |
let html = '';
|
| 1725 |
for (const [label, value] of fields) {
|
| 1726 |
+
html += '<div class="wizard-review-field"><span class="wizard-review-label">' + escapeHtml(label) + '</span><span class="wizard-review-value">' + escapeHtml(value || '\u2014') + '</span></div>';
|
| 1727 |
}
|
| 1728 |
el.innerHTML = html;
|
| 1729 |
const score = wizComputeScore();
|
|
|
|
| 1800 |
let html = '<div class="plan-card" id="active-plan-card">';
|
| 1801 |
html += '<div class="plan-card-title">\u25c6 PLAN READY FOR REVIEW</div>';
|
| 1802 |
for (const [label, value] of fields) {
|
| 1803 |
+
html += '<div class="wizard-review-field"><span class="wizard-review-label">' + escapeHtml(label) + '</span><span class="wizard-review-value">' + escapeHtml(value || '\u2014') + '</span></div>';
|
| 1804 |
}
|
| 1805 |
if (plan.machining_notes && plan.machining_notes.length) {
|
| 1806 |
+
html += '<div class="wizard-review-field"><span class="wizard-review-label">Notes</span><span class="wizard-review-value">' + escapeHtml(plan.machining_notes.join('; ')) + '</span></div>';
|
| 1807 |
}
|
| 1808 |
html += '<div class="plan-card-score">Score: ' + (plan.confidence_score || 0).toFixed(0) + '/8</div>';
|
| 1809 |
html += '<div class="plan-card-actions">';
|