Skip to content

fix(canvas): escape data.json before inlining it into the exported <script> - #17

Open
245678000000 wants to merge 1 commit into
CometixSpace:mainfrom
245678000000:fix/canvas-export-script-injection
Open

fix(canvas): escape data.json before inlining it into the exported <script>#17
245678000000 wants to merge 1 commit into
CometixSpace:mainfrom
245678000000:fix/canvas-export-script-injection

Conversation

@245678000000

@245678000000 245678000000 commented Aug 27, 2026

Copy link
Copy Markdown

Summary

buildSingleFileHtml in Cursor++/src/server/handlers/canvas/canvasStore.ts interpolates the canvas data.json straight into an inline <script> block:

data: new Map(Object.entries(${dataJson || '{}'})),

An HTML parser ends a script block at the first </script, regardless of whether it sits inside a JS string literal. data.json is user/model content — it arrives as DashboardService.ShareCanvas's data_json (services/account/DashboardService.ts:115) — so any string value containing </script> truncates the exported file and everything after it is parsed as DOM.

The sibling title in the same template already goes through escapeHtml; this path was missed.

Repro

const s = '{"a":"</script><h1>pwn</h1>"}';
'<script>window.x = new Map(Object.entries(' + s + '));</script>'
// -> <script>window.x = new Map(Object.entries({"a":"</script><h1>pwn</h1>"}));</script>
//                                                     ^ script block ends here

Fix

Escape < as the JSON escape \u003c before interpolating, via a small helper next to escapeHtml. In JSON < can only occur inside a string literal, so the escape is semantically identical — the data still parses back to exactly the same value. bundleBase64 needs no change (base64 has no <), and buildIndexHtml doesn't interpolate data at all.

Test plan

New Cursor++/src/server/tests/canvasExport.test.ts — round-trip of ordinary data, no new </script from a hostile payload, and value preservation after escaping. Two of the three fail without the source change (verified by stashing the fix).

  • pnpm --dir "Cursor++" run check-types
  • pnpm --dir "Cursor++" run lint
  • pnpm --dir "Cursor++" run test:server — 39 files / 370 tests passed (baseline on main is 38 / 367)

Those counts are from a machine with Cursor installed and a populated ~/.ccursor. On a clean checkout 5 of the 38 files fail for host-dependency reasons that have nothing to do with this change — 4 need @vscode/sqlite3 out of an installed Cursor, and protocol.test.ts:232 reads the real ~/.ccursor/knowledge-base.json. Detail in #20.

…cript>

buildSingleFileHtml interpolated the canvas data.json straight into an
inline <script> block:

    data: new Map(Object.entries(${dataJson || '{}'})),

An HTML parser ends a script block at the first `</script`, regardless of
whether it sits inside a JS string literal. data.json is user/model
content (it arrives as DashboardService.ShareCanvas's data_json), so any
string value containing `</script>` truncates the exported file and the
remainder is parsed as DOM. The sibling `title` was already run through
escapeHtml; this path was missed.

Escape `<` as the JSON escape U+003C before interpolating. In JSON `<`
can only occur inside a string literal, so the escape is semantically
identical and the data still parses back to the same value.

Adds canvasExport.test.ts: round-trip, no new `</script` from a hostile
payload, and value preservation. Two of the three fail without the fix.
Rogers-F added a commit to Rogers-F/CCursor that referenced this pull request Aug 29, 2026
- buildSummarySource: 总预算 min(0.6×窗口×4, 3.2e6) chars, 超限走官方
  CC-012 max-min 水位分配 (长度升序配额; <200 chars 整条丢弃打
  [omitted {role} message, N chars] 占位; user 截断保 <user_query> 块;
  Read/Task 截断强制保留路径清单 + error/fail 行 + 末段结论)
- 三级兜底 streamSummaryWithFallback / generateSummaryWithFallback:
  ≤3 次重试 (attempt≥2 附 "Write a shorter summary" + 源预算递减
  max(50K, min(÷2/÷3, 0.75×原长)); attempt 3 剥控制字符) → 确定性降级
  (不经模型, 水位拼接 + 注入防御声明, 预算 clamp(窗口×2%×4, 50K, 3.2M))
  → '- Prior conversation compacted.'; 两路 runtime 换用同一实现
  (流式 delta + heartbeat 保活)
- SUMMARY_HARD_CAP = 2×summaryReserve: 产出超限 → 一次 shorter-output
  重试 → 仍超则 token 级裁剪至 cap, 超支率进观测日志
- SUMMARY_USER_TEMPLATE 附加 [truncated]/[omitted] 工具输出段
  (SYSTEM_PROMPT 不动): 摘要须保留路径/命令/agentId 与结论,
  原文可重跑工具找回
- 测试: CometixSpace#11 摘要源封顶+路径错误行 / CometixSpace#16 三次失败→确定性降级含防御声明 /
  CometixSpace#17 水位分配 200 条不等长+user_query 存续 / #33 输出硬上界裁剪
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.

1 participant