Skip to content

Commit 24011e1

Browse files
committed
fix: harden overlay rendering
1 parent 3f25496 commit 24011e1

3 files changed

Lines changed: 72 additions & 132 deletions

File tree

Lines changed: 43 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,53 @@
1-
/*
2-
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3-
* SPDX-License-Identifier: Apache-2.0
1+
/**
2+
* Target path: apps/ui-tars/src/main/shared/setOfMarks.test.ts
43
*/
5-
import { test, expect } from 'vitest';
4+
import { describe, expect, it } from 'vitest';
65
import { setOfMarksOverlays } from './setOfMarks';
76

8-
const testMakeScreenMarker = () => {
9-
let xPos;
10-
let yPos;
11-
const actions = [
12-
{
13-
action_type: 'double_click',
14-
action_inputs: {
15-
start_box: '[0.1171875,0.20833333,0.1171875,0.20833333]',
16-
},
17-
reflection: 'reflection',
18-
thought: 'thought',
19-
},
20-
{
21-
action_type: 'type',
22-
action_inputs: {
23-
content: 'Hello, world!',
24-
},
25-
reflection: 'reflection',
26-
thought: 'thought',
27-
},
28-
{
29-
action_type: 'drag',
30-
action_inputs: {
31-
start_box: '[0.1171875,0.20833333,0.1171875,0.20833333]',
32-
end_box: '[0.175,0.647,0.175,0.647]',
33-
},
34-
reflection: 'reflection',
35-
thought: 'thought',
36-
},
37-
{
38-
reflection: '',
39-
thought:
40-
'我已经在搜索框中输入了"杭州天气",但还需要按下回车键来执行搜索。现在需要按下回车键来提交搜索请求,这样就能看到杭州的天气信息。',
41-
action_type: 'hotkey',
42-
action_inputs: { key: 'ctrl enter' },
43-
},
44-
{
45-
reflection: '',
46-
thought:
47-
'To narrow down the search results to cat litters within the specified price range of $18 to $32, I need to adjust the price filter. The next logical step is to drag the left handle of the price slider to set the minimum price to $18, ensuring that only products within the desired range are displayed.\n' +
48-
'Drag the left handle of the price slider to set the minimum price to $18.',
49-
action_type: 'drag',
50-
action_inputs: {
51-
start_box: '[0.072,0.646,0.072,0.646]',
52-
end_box: '[0.175,0.647,0.175,0.647]',
7+
const screenshotContext = {
8+
size: { width: 1920, height: 1080 },
9+
} as any;
10+
11+
describe('setOfMarksOverlays SVG escaping', () => {
12+
it('escapes content for type action', () => {
13+
const predictions = [
14+
{
15+
action_type: 'type',
16+
action_inputs: {
17+
content: '<img src=x onerror=alert(1)>',
18+
},
5319
},
54-
},
55-
{
56-
reflection: null,
57-
thought:
58-
'我看到桌面上有Google Chrome的图标,要完成打开Chrome的任务,我需要双击该图标。在之前的操作中,我已经双击了Chrome图标,但是页面没有发生变化,我应该等待一段时间,等待页面加载完成。',
59-
action_type: 'wait',
60-
action_inputs: {},
61-
},
62-
];
63-
for (const action of actions) {
20+
] as any;
21+
6422
const { overlays } = setOfMarksOverlays({
65-
predictions: [action],
66-
screenshotContext: {
67-
size: {
68-
width: 2560,
69-
height: 1440,
23+
predictions,
24+
screenshotContext,
25+
xPos: 100,
26+
yPos: 100,
27+
});
28+
29+
expect(overlays[0].svg).toContain('&lt;img');
30+
expect(overlays[0].svg).not.toContain('<img');
31+
});
32+
33+
it('escapes hotkey text', () => {
34+
const predictions = [
35+
{
36+
action_type: 'hotkey',
37+
action_inputs: {
38+
key: 'ctrl <script>alert(1)</script>',
7039
},
71-
scaleFactor: 1,
7240
},
73-
xPos,
74-
yPos,
41+
] as any;
42+
43+
const { overlays } = setOfMarksOverlays({
44+
predictions,
45+
screenshotContext,
46+
xPos: 100,
47+
yPos: 100,
7548
});
76-
console.log('overlays', overlays);
77-
// for (let i = 0; i < overlays.length; i++) {
78-
// const overlay = overlays[i];
79-
// const currentOverlay = new BrowserWindow({
80-
// width: overlay.boxWidth || 200,
81-
// height: overlay.boxHeight || 200,
82-
// transparent: true,
83-
// frame: false,
84-
// alwaysOnTop: true,
85-
// skipTaskbar: true,
86-
// focusable: false,
87-
// hasShadow: false,
88-
// thickFrame: false,
89-
// paintWhenInitiallyHidden: true,
90-
// type: 'panel',
91-
// webPreferences: {
92-
// nodeIntegration: true,
93-
// contextIsolation: false,
94-
// },
95-
// });
96-
// currentOverlay.webContents.openDevTools();
97-
// if (overlay.xPos && overlay.yPos && overlay.svg) {
98-
// currentOverlay.setPosition(
99-
// overlay.xPos + overlay.offsetX,
100-
// overlay.yPos + overlay.offsetY,
101-
// );
102-
// xPos = overlay.xPos;
103-
// yPos = overlay.yPos;
104-
// currentOverlay.loadURL(`data:text/html;charset=UTF-8,
105-
// <html>
106-
// <head>
107-
// <style>
108-
// html, body {
109-
// background: transparent;
110-
// margin: 0;
111-
// padding: 0;
112-
// overflow: hidden;
113-
// width: 100%;
114-
// height: 100%;
115-
// }
116-
// </style>
117-
// </head>
118-
// <body>
119-
// ${overlay.svg}
120-
// </body>
121-
// </html>
122-
// `);
123-
// }
124-
// await sleep(1000);
125-
// currentOverlay.close();
126-
// }
127-
}
128-
};
12949

130-
test('not throw error', () => {
131-
expect(() => testMakeScreenMarker()).not.toThrow();
50+
expect(overlays[0].svg).toContain('&lt;script&gt;');
51+
expect(overlays[0].svg).not.toContain('<script>');
52+
});
13253
});

apps/ui-tars/src/main/shared/setOfMarks.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export interface Overlay {
1717
svg: string;
1818
}
1919

20+
const escapeSvgText = (value: string): string =>
21+
value
22+
.replace(/&/g, '&amp;')
23+
.replace(/</g, '&lt;')
24+
.replace(/>/g, '&gt;')
25+
.replace(/"/g, '&quot;')
26+
.replace(/'/g, '&#39;');
27+
2028
/**
2129
* set of marks overlays, action highlights
2230
* @param predictions PredictionParsed[]
@@ -40,6 +48,7 @@ export const setOfMarksOverlays = ({
4048
const { width, height } = screenshotContext?.size || {};
4149

4250
for (const prediction of predictions) {
51+
const safeActionType = escapeSvgText(String(prediction.action_type || ''));
4352
let boxWidth: number;
4453
let boxHeight: number;
4554
switch (prediction.action_type) {
@@ -100,11 +109,11 @@ export const setOfMarksOverlays = ({
100109
x="${boxWidth / 2 + 65}"
101110
y="${boxHeight / 2}"
102111
font-family="-apple-system, BlinkMacSystemFont, Arial, sans-serif"
103-
font-size="16"
104-
fill="red"
105-
text-anchor="middle"
106-
dominant-baseline="middle"
107-
>${prediction.action_type}</text>
112+
font-size="16"
113+
fill="red"
114+
text-anchor="middle"
115+
dominant-baseline="middle"
116+
>${safeActionType}</text>
108117
</svg>`,
109118
});
110119
}
@@ -114,6 +123,7 @@ export const setOfMarksOverlays = ({
114123
boxHeight = 100;
115124

116125
const { content } = prediction.action_inputs || {};
126+
const safeContent = escapeSvgText(String(content || ''));
117127

118128
overlays.push({
119129
prediction,
@@ -132,7 +142,7 @@ export const setOfMarksOverlays = ({
132142
fill="red"
133143
text-anchor="middle"
134144
dominant-baseline="middle"
135-
>Typing: "${content}"</text>
145+
>Typing: "${safeContent}"</text>
136146
</svg>`,
137147
});
138148
break;
@@ -142,6 +152,7 @@ export const setOfMarksOverlays = ({
142152

143153
const { key = '' } = prediction.action_inputs || {};
144154
const keys = key.split(' ').join(' + ');
155+
const safeKeys = escapeSvgText(keys);
145156

146157
overlays.push({
147158
prediction,
@@ -160,7 +171,7 @@ export const setOfMarksOverlays = ({
160171
fill="red"
161172
text-anchor="middle"
162173
dominant-baseline="middle"
163-
>Hotkey: ${keys}</text>
174+
>Hotkey: ${safeKeys}</text>
164175
</svg>`,
165176
});
166177
break;
@@ -195,7 +206,7 @@ export const setOfMarksOverlays = ({
195206
fill="red"
196207
text-anchor="middle"
197208
dominant-baseline="middle"
198-
>${prediction.action_type}</text>
209+
>${safeActionType}</text>
199210
</svg>`,
200211
});
201212
break;

apps/ui-tars/src/main/window/ScreenMarker.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ class ScreenMarker {
5959
thickFrame: false,
6060
paintWhenInitiallyHidden: true,
6161
type: 'panel',
62-
webPreferences: { nodeIntegration: true, contextIsolation: false },
62+
webPreferences: {
63+
nodeIntegration: false,
64+
contextIsolation: true,
65+
sandbox: true,
66+
},
6367
});
6468

6569
this.screenWaterFlow.setFocusable(false);
@@ -226,7 +230,11 @@ class ScreenMarker {
226230
thickFrame: false,
227231
paintWhenInitiallyHidden: true,
228232
type: 'panel',
229-
webPreferences: { nodeIntegration: true, contextIsolation: false },
233+
webPreferences: {
234+
nodeIntegration: false,
235+
contextIsolation: true,
236+
sandbox: true,
237+
},
230238
...(overlay.xPos &&
231239
overlay.yPos && {
232240
// logical pixels

0 commit comments

Comments
 (0)