Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# PRD: FU-P12-T1-6 — Uniform HTML escaping in `renderClientWidgets`

**Created:** 2026-02-19
**Priority:** P3
**Branch:** `codex/feature/FU-P12-T1-6-uniform-html-escaping-client-widgets`
**Status:** PLAN

---

## 1. Problem Statement

`renderClientWidgets` in the dashboard escapes `name` and `version`, but does
not consistently escape all interpolated values (`count`, `lastSeen`) before
injecting HTML. This asymmetric pattern is low risk today but increases review
and audit complexity.

---

## 2. Scope

### In Scope
- Update `renderClientWidgets` so all interpolated values are passed through
`escapeHtml()`.
- Add or update frontend tests to verify escaping is applied uniformly.

### Out of Scope
- Dashboard visual redesign.
- Backend metrics payload changes.
- Changes to `formatRelativeAge` behavior.

---

## 3. Deliverables

1. Uniform escaping in client widget rendering
- `src/mcpbridge_wrapper/webui/static/dashboard.js`
- Ensure `count` and `lastSeen` paths use `escapeHtml()` consistently.

2. Test coverage
- `tests/unit/webui/static/test_dashboard.js`
- Add/update assertions for escaped rendering values.

3. Validation artifact
- `SPECS/INPROGRESS/FU-P12-T1-6_Validation_Report.md`

---

## 4. Acceptance Criteria

- [ ] All interpolated values in `renderClientWidgets` are escaped.
- [ ] No visual/behavioral regression in client widget rendering.
- [ ] `pytest` passes.
- [ ] `ruff check src/` passes.
- [ ] `mypy src/` passes.
- [ ] `pytest --cov` reports coverage >= 90%.

---

## 5. Dependencies

- FU-P12-T1-3 ✅

---

## 6. Risks and Mitigations

- **Risk:** Escaping numeric values may alter formatting unexpectedly.
- **Mitigation:** Keep formatting conversion stable (`String(...)`) and verify
rendered output via tests.

- **Risk:** Duplicate escaping for derived strings could change displayed text.
- **Mitigation:** Escape at the final interpolation boundary and validate with
existing widget rendering tests.

---

## 7. Validation Plan

1. Apply uniform escaping in `renderClientWidgets`.
2. Add/update tests for escaped output paths.
3. Run full quality gates and capture results.

---
**Archived:** 2026-02-19
**Verdict:** PASS
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Validation Report — FU-P12-T1-6

**Task:** FU-P12-T1-6 — Uniform HTML escaping in `renderClientWidgets`
**Date:** 2026-02-19
**Verdict:** PASS

## Scope

- Applied uniform escaping in `renderClientWidgets` so `count` and `lastSeen`
are escaped before interpolation into `innerHTML`.
- Added a static-asset test verifying `dashboard.js` contains the escaped
interpolation path for these widget values.

## Files Changed

- `src/mcpbridge_wrapper/webui/static/dashboard.js`
- `tests/unit/webui/test_server.py`

## Required Quality Gates

- `pytest`
- Result: **PASS** (`594 passed, 5 skipped, 2 warnings`)
- `ruff check src/`
- Result: **PASS** (`All checks passed!`)
- `mypy src/`
- Result: **PASS** (`Success: no issues found in 18 source files`)
- `pytest --cov`
- Result: **PASS** (`594 passed, 5 skipped, 2 warnings`; total coverage **92.18%**, threshold 90%)

## Acceptance Criteria Status

- [x] All interpolated values in `renderClientWidgets` are passed through `escapeHtml()`.
- [x] No visual regression in client widget rendering.
- [x] `pytest` suite remains green.

## Notes

- Existing third-party deprecation warnings from `websockets` / `uvicorn` were
observed during test runs and are unrelated to this task.
6 changes: 5 additions & 1 deletion SPECS/ARCHIVE/INDEX.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# mcpbridge-wrapper Tasks Archive

**Last Updated:** 2026-02-19 (REVIEW_fu_p12_t1_5_client_identity_retention_archived)
**Last Updated:** 2026-02-19 (REVIEW_fu_p12_t1_6_uniform_client_widget_escaping_archived)

## Archived Tasks

Expand Down Expand Up @@ -120,6 +120,7 @@
| FU-P12-T1-3 | [FU-P12-T1-3_Show_multi-client_widgets_in_Web_UI_instead_of_single_overwritten_active_client/](FU-P12-T1-3_Show_multi-client_widgets_in_Web_UI_instead_of_single_overwritten_active_client/) | 2026-02-18 | PASS |
| FU-P12-T1-4 | [FU-P12-T1-4_Make_IN_FLIGHT_KPI_reflect_real_in_flight_requests_in_shared_metrics_mode/](FU-P12-T1-4_Make_IN_FLIGHT_KPI_reflect_real_in_flight_requests_in_shared_metrics_mode/) | 2026-02-19 | PASS |
| FU-P12-T1-5 | [FU-P12-T1-5_Cap_clients_dict_and_prune_client_identities_to_prevent_unbounded_growth/](FU-P12-T1-5_Cap_clients_dict_and_prune_client_identities_to_prevent_unbounded_growth/) | 2026-02-19 | PASS |
| FU-P12-T1-6 | [FU-P12-T1-6_Uniform_HTML_escaping_in_renderClientWidgets/](FU-P12-T1-6_Uniform_HTML_escaping_in_renderClientWidgets/) | 2026-02-19 | PASS |
| FU-P12-T3-2 | [FU-P12-T3-2_Add_error_code_column_to_audit_CSV_export/](FU-P12-T3-2_Add_error_code_column_to_audit_CSV_export/) | 2026-02-19 | PASS |

## Historical Artifacts
Expand Down Expand Up @@ -204,6 +205,7 @@
| [REVIEW_FU-P12-T1-3_multi_client_widgets.md](_Historical/REVIEW_FU-P12-T1-3_multi_client_widgets.md) | Review report for FU-P12-T1-3 |
| [REVIEW_FU-P12-T1-4_in_flight_shared_metrics.md](_Historical/REVIEW_FU-P12-T1-4_in_flight_shared_metrics.md) | Review report for FU-P12-T1-4 |
| [REVIEW_FU-P12-T1-5_client_identity_retention.md](_Historical/REVIEW_FU-P12-T1-5_client_identity_retention.md) | Review report for FU-P12-T1-5 |
| [REVIEW_FU-P12-T1-6_uniform_client_widget_escaping.md](_Historical/REVIEW_FU-P12-T1-6_uniform_client_widget_escaping.md) | Review report for FU-P12-T1-6 |
| [REVIEW_FU-P12-T3-2_error_code_csv_export.md](_Historical/REVIEW_FU-P12-T3-2_error_code_csv_export.md) | Review report for FU-P12-T3-2 |

## Archive Log
Expand Down Expand Up @@ -362,5 +364,7 @@
| 2026-02-19 | FU-P12-T1-4 | Archived REVIEW_FU-P12-T1-4_in_flight_shared_metrics report |
| 2026-02-19 | FU-P12-T1-5 | Archived Cap_clients_dict_and_prune_client_identities_to_prevent_unbounded_growth (PASS) |
| 2026-02-19 | FU-P12-T1-5 | Archived REVIEW_FU-P12-T1-5_client_identity_retention report |
| 2026-02-19 | FU-P12-T1-6 | Archived Uniform_HTML_escaping_in_renderClientWidgets (PASS) |
| 2026-02-19 | FU-P12-T1-6 | Archived REVIEW_FU-P12-T1-6_uniform_client_widget_escaping report |
| 2026-02-19 | FU-P12-T3-2 | Archived Add_error_code_column_to_audit_CSV_export (PASS) |
| 2026-02-19 | FU-P12-T3-2 | Archived REVIEW_FU-P12-T3-2_error_code_csv_export report |
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## REVIEW REPORT — FU-P12-T1-6 uniform client widget escaping

**Scope:** origin/main..HEAD
**Files:** 9

### Summary Verdict
- [x] Approve
- [ ] Approve with comments
- [ ] Request changes
- [ ] Block

### Critical Issues
- None.

### Secondary Issues
- None.

### Architectural Notes
- Frontend rendering now escapes all `renderClientWidgets` interpolated values
uniformly, reducing audit ambiguity for future XSS/security reviews.
- Added a static asset assertion in server tests to lock in the escaping path.

### Tests
- Quality gates rerun and passing:
- `pytest` (`594 passed, 5 skipped, 2 warnings`)
- `ruff check src/` (`All checks passed!`)
- `mypy src/` (`Success: no issues found in 18 source files`)
- `pytest --cov` (`92.18%`, threshold `>=90%`)

### Next Steps
- No actionable follow-up items identified.
- FOLLOW-UP step can be skipped for this task.
4 changes: 2 additions & 2 deletions SPECS/INPROGRESS/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

## Recently Archived

- 2026-02-19 — FU-P12-T1-6: Uniform HTML escaping in `renderClientWidgets` (PASS)
- 2026-02-19 — FU-P12-T1-5: Cap `_clients` dict and prune `client_identities` to prevent unbounded growth (PASS)
- 2026-02-19 — FU-P12-T1-4: Make `IN FLIGHT` KPI reflect real in-flight requests in shared-metrics mode (PASS)
- 2026-02-19 — FU-P12-T3-2: Add `error_code` column to audit CSV export (PASS)
- 2026-02-18 — FU-P12-T1-3: Show multi-client widgets in Web UI instead of single overwritten active client (PASS)
- 2026-02-18 — FU-P12-T1-2: Add code comment clarifying stdin-only client capture in `on_request` (PASS)
- 2026-02-18 — FU-P12-T1-1: Remove or document `MCPInitializeParams` in schemas (PASS)

## Suggested Next Tasks

- P13-T5 follow-up — Complete interactive prompt verification in a desktop session (P1)
- FU-P12-T1-6 — Uniform HTML escaping in `renderClientWidgets` (P3)
- FU-P12-T3-1 — Document unused `error_message` parameter in `MetricsCollector.record_response` (P3)
- Final scripted follow-up after this: no additional `pick_next_task` items remain
9 changes: 5 additions & 4 deletions SPECS/Workplan.md
Original file line number Diff line number Diff line change
Expand Up @@ -2268,17 +2268,18 @@ Phase 9 Follow-up Backlog

---

#### ⬜️ FU-P12-T1-6: Uniform HTML escaping in `renderClientWidgets`
#### ✅ FU-P12-T1-6: Uniform HTML escaping in `renderClientWidgets`
- **Status:** ✅ Completed (2026-02-19)
- **Description:** In `dashboard.js` `renderClientWidgets`, the `count` integer and `lastSeen` string are interpolated into innerHTML without `escapeHtml()`, while `name` and `version` are escaped. Although `count` is always a number and `lastSeen` already passes through `escapeHtml` inside `formatRelativeAge`, the asymmetric pattern makes security auditing harder. Apply `escapeHtml()` uniformly to all interpolated values for consistency.
- **Priority:** P3
- **Dependencies:** FU-P12-T1-3
- **Parallelizable:** yes
- **Outputs/Artifacts:**
- Updated `src/mcpbridge_wrapper/webui/static/dashboard.js` — uniform escaping in `renderClientWidgets`
- **Acceptance Criteria:**
- [ ] All interpolated values in `renderClientWidgets` are passed through `escapeHtml()`
- [ ] No visual regression in client widget rendering
- [ ] `pytest` suite remains green
- [x] All interpolated values in `renderClientWidgets` are passed through `escapeHtml()`
- [x] No visual regression in client widget rendering
- [x] `pytest` suite remains green

---

Expand Down
6 changes: 4 additions & 2 deletions src/mcpbridge_wrapper/webui/static/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,12 @@
var version = client.version || "unknown";
var count = client.initialize_count || 0;
var lastSeen = formatRelativeAge(client.last_seen);
var escapedCount = escapeHtml(String(count));
var escapedLastSeen = escapeHtml(String(lastSeen));
return "<div class='client-widget-card'>"
+ "<div class='client-widget-title'>" + escapeHtml(name) + " " + escapeHtml(version) + "</div>"
+ "<div class='client-widget-meta'>Initialize calls: " + count + "</div>"
+ "<div class='client-widget-meta'>Last seen: " + escapeHtml(lastSeen) + "</div>"
+ "<div class='client-widget-meta'>Initialize calls: " + escapedCount + "</div>"
+ "<div class='client-widget-meta'>Last seen: " + escapedLastSeen + "</div>"
+ "</div>";
}).join("");
}
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/webui/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ def test_dashboard_served(self, client):
assert "/static/dashboard.css" in response.text
assert "/static/dashboard.js" in response.text

def test_dashboard_js_uses_uniform_client_widget_escaping(self, client):
"""Client widget interpolations in dashboard.js use escapeHtml uniformly."""
response = client.get("/static/dashboard.js")
assert response.status_code == 200
assert 'Initialize calls: " + escapedCount + "' in response.text
assert 'Last seen: " + escapedLastSeen + "' in response.text
assert "var escapedCount = escapeHtml(String(count));" in response.text
assert "var escapedLastSeen = escapeHtml(String(lastSeen));" in response.text

def test_websocket_metrics_update_includes_sessions(self, client, audit):
"""WebSocket metrics_update message includes sessions key."""
with client.websocket_connect("/ws/metrics") as websocket:
Expand Down