CallMeDaniel Claude Opus 4.6 (1M context) commited on
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 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: str = "exploring"
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 = OUTPUT_DIR / f"{name}.stl"
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 = OUTPUT_DIR / f"{name}.step"
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 = OUTPUT_DIR / f"{name}.gcode"
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 = OUTPUT_DIR / f"{name}.3mf"
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">';