Repository navigation
Conversation
Make tracer serve loopback-only with opt-in CORS and request/batch caps, escape untrusted report/scan content (including sankey/viz), and allowlist watch sink filenames so local telemetry cannot traverse the filesystem. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens TRACER’s local HTTP serving defaults and closes multiple injection sinks by making tracer serve loopback-by-default with opt-in CORS and request caps, tightening file naming to prevent watch path traversal, and escaping/sanitizing untrusted fields in generated HTML (fit reports and scan viz). It also documents the operator trust model (notably: serve has no auth and .tracer/pipeline.joblib is trusted pickle code).
Changes:
- Harden
tracer serve: bind127.0.0.1by default, opt-in CORS, body/batch/element caps, and close connections on error responses. - Prevent path traversal in watch sinks via allowlisted watcher names (Python + JS) and root-escape checks.
- Close stored-XSS/script-breakout vectors in HTML outputs (fit report fields, sankey labels, scan viz embedded JSON) and add/extend security documentation and tests.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_watch.py | Adds regression tests for rejecting unsafe watcher names and allowing safe names. |
| tests/test_serve.py | Updates serve tests to use fit/FitConfig directly and adds tests for loopback default, body/batch caps, CORS default-off, and Connection: close on errors. |
| tests/test_html_report_escape.py | New tests covering HTML report escaping, sankey label sanitization, and scan viz JSON escaping. |
| src/tracer/watch.py | Adds allowlisted watcher-name validation and prevents file path escape from the watch directory. |
| src/tracer/scanner.py | Escapes embedded viz JSON to prevent </script> breakout; hardens HTML escaping helper. |
| src/tracer/runtime/serve.py | Implements loopback-by-default serving, opt-in CORS/preflight, request caps, and error-response connection closing. |
| src/tracer/policy/artifacts.py | Adds explicit warning docstring about pickle/joblib trust boundary when loading pipelines. |
| src/tracer/cli/main.py | Wires new serve CLI flags (host default, cors origin, body cap, batch cap) into runtime serve. |
| src/tracer/analysis/sankey.py | Sanitizes node labels to prevent markup/script injection in Plotly HTML. |
| src/tracer/analysis/html_report.py | Escapes/coerces dynamic manifest/report fields; pins Plotly CDN version and injects only when needed. |
| SECURITY.md | Documents operator trust model: .tracer artifacts are trusted code; serve has no auth; watch can exfiltrate. |
| js/test/watch.test.ts | Adds JS tests for rejecting unsafe watch names. |
| js/src/index.ts | Adds JS allowlist validator for watch filenames and prevents directory escape via resolved path checks. |
| docs/javascript.md | Updates docs to reflect tracer serve now binds to 127.0.0.1 by default and recommends reverse proxy for 0.0.0.0. |
| docs/cli.md | Documents new/default serve flags and hardened defaults. |
| AGENTS.md | Updates agent-facing docs to reflect hardened serve defaults and trust boundaries. |
| .gitignore | Adds patterns to prevent committing secrets/credentials and local tooling artifacts. |
Suppressed comments (2)
src/tracer/runtime/serve.py:106
/predictreturns HTTP 500 for client-caused validation failures (e.g., bad embedding shape/dtype or dimension mismatch). These should be 400-series responses so callers can distinguish invalid input from server faults.
except Exception as e:
self._json_response(500, {"error": str(e)})
src/tracer/analysis/html_report.py:437
int(s["count"])in the length-bucket table can raise ifcountis missing or not an integer, causing the HTML report generation to fail. Coerce defensively (as with manifest fields) rather than crashing on malformed artifacts.
for s in length_slices:
html += (
f'<tr><td>{_esc(s["slice_name"])}</td>'
f'<td><b style="color:#f0f6fc">{s["handled_rate"]:.1%}</b></td>'
f'<td>{_bar_html(s["handled_rate"])}</td>'
f'<td style="color:#8b949e">{int(s["count"])}</td></tr>\n'
)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| except Exception as e: | ||
| self._json_response(500, {"error": str(e)}) |
| f'<td style="color:#8b949e">{int(s["count"])}</td>' | ||
| f'<td style="color:#8b949e">{_esc(ta_str)}</td></tr>\n' |
|
@adrida hey is tracer open to contribute ? |
Summary
tracer serveloopback-by-default (127.0.0.1), with opt-in CORS, request body / batch / embedding-element caps, socket timeouts, andConnection: closeon error responses — so a local demo server is not an unauthenticated LAN/WAN endpoint by accident.\u003c-safe viz JSON) and path traversal inwatchLocalFileSink via an allowlisted filename (Python +@tracer-llm/watch).SECURITY.md(serve has no auth;.tracer/pipeline.joblibis trusted code / pickle). This PR does not eliminate pickle RCE — it makes the trust boundary explicit.Why this shape
One reviewable theme: secure-by-default local surface + injection sink closure. Follow-ups intentionally deferred: Actions SHA pinning, cloud login/session/
put_urlhardening, artifact signing.Compatibility notes
tracer serve/serve()now bind127.0.0.1(was0.0.0.0). Use--host 0.0.0.0only behind a reverse proxy with auth/TLS.Access-Control-Allow-Originunless--cors-originis set (related enhancement #71 / #70 should keep opt-in defaults, not restore*).[A-Za-z0-9][A-Za-z0-9._-]{0,127}.Test plan
pytest tests/test_serve.py tests/test_watch.py tests/test_html_report_escape.py -v(all passed locally)tracer serve --helpshows host default127.0.0.1and documents--cors-origin/ body capsembedding_dim— no raw<script>/<img onerror>LocalFileSink("../evil")raises; safe names still write under the watch dir--host 0.0.0.0, warning is printed; without auth this remains operator responsibilityMade with Cursor