Skip to content

Replace eval-based JS interop with named functions in polypilot-interop.js#219

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/replace-eval-with-js-module
Draft

Replace eval-based JS interop with named functions in polypilot-interop.js#219
Copilot wants to merge 2 commits intomainfrom
copilot/replace-eval-with-js-module

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 26, 2026

~26 instances of JS.InvokeVoidAsync("eval", $"...") across 5 Razor files interpolated C# values directly into JS strings — an XSS/injection surface. Replaced all of them with parameterized calls to named global functions.

New: wwwroot/js/polypilot-interop.js

Named functions covering every eval use case:

Function Replaces
setThemeAttribute(theme) eval($"...setAttribute('data-theme', '{dataTheme}')")
startSidebarResize(startX) eval($"...var startX = {e.ClientX}...")
setInputValue(id, value, cursorAtStart) eval($"...'{inputId}'...{JsonSerializer.Serialize(text)}...")
showPopup(trigger, headerHtml, contentHtml) 40-line eval IIFE for skills/agents popups
showPromptsPopup(..., sessionName) Same + click handler; uses CSS.escape() for safe selector construction
ensureDashboardKeyHandlers() ~90-line eval keydown registration block
focusAndSelect(id), focusAndSetValue(id, val) Various getElementById + focus/value evals
clearDashRef(), clearSettingsRef() Null-out globals
wireSettingsSearch(), wireSessionNameInputEnter() One-shot event listener setup
ensureTextareaAutoResize(), ensureLoadMoreObserver(), scrollMessagesToBottom() Lazy init / scroll helpers

Pre-defines window.__setDashRef and window.__setSettingsRef so the evals that dynamically defined them are removed entirely.

Call-site changes

Before:

var inputId = $"input-{sessionName.Replace(" ", "-")}";
var cursorExpr = cursorAtStart ? "0" : "el.value.length";
await JS.InvokeVoidAsync("eval",
    $"(function(){{ var el = document.getElementById('{inputId}'); " +
    $"if(el){{ el.value = {JsonSerializer.Serialize(text)}; var p = {cursorExpr}; el.setSelectionRange(p, p); }} }})()");

After:

var inputId = $"input-{sessionName.Replace(" ", "-")}";
await JS.InvokeVoidAsync("setInputValue", inputId, text, cursorAtStart);

Cleanup

  • EscapeForJs helper in ExpandedSessionView.razor removed (was only needed to embed C# values into JS strings)
  • Popup methods no longer call EscapeForJs on HTML before passing it — strings are now passed as parameters, not code
Original prompt

This section details on the original issue you should resolve

<issue_title>Replace eval-based JS interop with dedicated JS module</issue_title>
<issue_description>## Problem

The codebase has ~30 instances of \JS.InvokeVoidAsync("eval", $"...");\ across Dashboard.razor, Settings.razor, SessionSidebar.razor, ExpandedSessionView.razor, MainLayout.razor, and SessionListItem.razor. This pattern:

  1. XSS risk: Several calls interpolate C# values directly into JS strings (e.g. \$"...'{inputId}'...", \$"...'{dataTheme}'..."). If any value originates from user input, it could inject arbitrary JS.
  2. Prompt injection surface: Dashboard.razor sends user prompts through eval-based JS interop in the orchestrator routing path.
  3. Maintainability: Inline JS strings are hard to debug, lint, or test.

Proposed Fix

Create a dedicated \wwwroot/js/polypilot-interop.js\ module with named functions:
\\js
export function portalElement(selector) { ... }
export function setThemeAttribute(theme) { ... }
export function focusAndSetValue(elementId, value) { ... }
\\

Then call via \JS.InvokeVoidAsync("portalElement", selector)\ — parameterized, no eval, no injection.

Scope

~30 eval calls across 6 files. Can be done incrementally per-file.

Found during multi-model review of PR #205.</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace eval-based JS interop with dedicated JS module Replace eval-based JS interop with named functions in polypilot-interop.js Feb 26, 2026
@PureWeen
Copy link
Copy Markdown
Owner

PureWeen commented Apr 3, 2026

🔍 Multi-Model Code Review — PR #219

Reviewed by: Claude Opus 4.6, Claude Sonnet 4.6, GPT-5.3 Codex
CI Status: ⚠️ No checks reported on branch
Prior Reviews: None
Verification: All findings independently verified against both the PR branch and current main

Overall this is a strong security improvement — replacing ~26 eval() calls with parameterized JS function calls eliminates the injection surface. The JS functions faithfully reproduce the original behavior, CSS.escape() in showPromptsPopup is better than the old EscapeForJs, and innerHTML usage is safe (all dynamic values pass through EscapeHtml() in C#). The fallback rect in popup functions also fixes a pre-existing NaN positioning bug.


🔴 CRITICAL — setAppFontSize not defined in polypilot-interop.js

Files: MainLayout.razor (~line 85), Settings.razor (~line 973)
Flagged by: Opus ✓ | Verified by grep ✓

Both MainLayout.ApplyFontSize() and Settings.ApplyFontSize() call:

await JS.InvokeVoidAsync("setAppFontSize", fontSize);

But window.setAppFontSize is never defined in polypilot-interop.js. This throws a JSException (TypeError) on first load and on every font size change.

Fix — add to polypilot-interop.js:

window.setAppFontSize = function(size) {
    document.documentElement.style.setProperty('--app-font-size', size + 'px');
};

🔴 CRITICAL — clearElementValue not defined in polypilot-interop.js

File: Settings.razor (~line 647)
Flagged by: Opus ✓ | Verified by grep ✓

await JS.InvokeVoidAsync("clearElementValue", "settings-search");

But window.clearElementValue is never defined. Clicking the search clear button in Settings throws a JSException.

Fix — add to polypilot-interop.js:

window.clearElementValue = function(elementId) {
    var el = document.getElementById(elementId);
    if (el) el.value = '';
};

🟡 MODERATE — PR needs rebase; main has diverged significantly

Flagged by: Sonnet ✓ | Verified ✓

Current main has added several features to ExpandedSessionView.razor since this PR branched:

  1. Agent click-to-insert handlerShowAgentsPopup on main now has a .agent-row click handler that inserts @agentname into the textarea. The PR's conversion to showPopup() (which only has stopPropagation) was correct for the code it branched from, but will lose the new agent click behavior after rebase. A showAgentsPopup function (similar to showPromptsPopup) will be needed.
  2. ShowLogPopup + template callersmain now has EscapeForJs callers at lines 273, 862, and 1053-1055 (log popup feature). Since the PR removes the EscapeForJs definition, rebase will produce a compile error until these are also migrated.

The PR compiles on its own branch (verified — zero EscapeForJs references remain). This is a rebase issue, not a bug in the PR.


🟢 MINOR — Double semicolon in Settings.razor

File: Settings.razor (~line 716)
Flagged by: Opus ✓, Sonnet ✓

await JS.InvokeVoidAsync("wireSettingsSearch");;

Extra ; — harmless but a typo.


✅ What looks correct (all 3 models agree)

  • All 18 defined JS functions faithfully reproduce the original eval behavior
  • __setDashRef / __setSettingsRef correctly pre-defined at load time
  • showPopup / showPromptsPopup innerHTML is safe — dynamic values HTML-escaped in C#
  • showPromptsPopup uses CSS.escape(sessionName) — improvement over old EscapeForJs
  • wireSessionNameInputEnter adds __enterWired idempotency guard — improvement
  • ensureDashboardKeyHandlers keyboard shortcuts are behavior-identical (var renames to avoid shadowing)
  • polypilot-interop.js loads before Blazor init in index.html — correct ordering
  • EscapeForJs removal is safe on the PR branch (no remaining callers)

Test Coverage

No new tests, but this is a mechanical refactor. The missing function definitions (#1, #2) are the main risk — once added, the refactor is behavior-preserving.


Summary

# Severity Finding Models
1 🔴 setAppFontSize undefined — runtime crash on font size Opus + verified
2 🔴 clearElementValue undefined — runtime crash on search clear Opus + verified
3 🟡 Needs rebase — agent click handler + ShowLogPopup callers diverged Sonnet + verified
4 🟢 Double semicolon typo Opus + Sonnet

Recommended action: ⚠️ Request changes

  1. Add the 2 missing function definitions to polypilot-interop.js
  2. Rebase onto current main and migrate the new ShowAgentsPopup click handler + ShowLogPopup callers
  3. Fix double semicolon

Once addressed, this is a solid security improvement ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace eval-based JS interop with dedicated JS module

2 participants