Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Starting from 0.2.0, CLI / Extension / DSH Plugin share the same version number.

### Fixed

- Saved HTML debug captures now redact sensitive initial values in `textarea` and
`select` controls as well as `input` controls, including encoded field names
and autocomplete tokens.
- CLI cancellation keeps the original RPC progressing and retries cancellation
that arrives before daemon registration, reducing background screenshots that
keep a session busy after the caller stops. Confirmation and response waiting
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ BrowserSkill 的重要变更记录于此,格式参考 [Keep a Changelog](https

### 修复

- 保存的 HTML 调试捕获现在也会对 `textarea` 和 `select` 控件中的敏感初始值脱敏,支持编码后的字段名和 `autocomplete` 令牌。
- CLI 取消时继续推进原 RPC,并重试先于 daemon 注册到达的取消请求,减少调用方停止后后台截图仍占用会话的情况。取消确认和响应等待共用两秒预算,MCP 取消也使用同一预算;文件传输清理保留独立的有限预算。超过该期限才注册的请求仍需 daemon 侧取消追踪解决([#419](https://github.com/Tencent/BrowserSkill/pull/419))。
- 保存的 HTML 调试捕获在 `autocomplete` 含首尾空白或多个令牌时(如 `section-login one-time-code` 或 `section-checkout billing cc-number`)也会对敏感输入值脱敏;`section-login email` 等非敏感令牌列表保持不变([#566](https://github.com/Tencent/BrowserSkill/pull/566))。
- 处理旧版 `mousewheel` 事件的页面(如哔哩哔哩播放器的音量调节)重新能收到滚轮输入。此前扩展即使没有共享控制会话也会在每个页面监听 `wheel`,导致 Chromium 跳过这些处理函数;现在只在采集期间于 `window` 上监听。
Expand Down
35 changes: 35 additions & 0 deletions apps/extension/src/debug/__tests__/redact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,41 @@ describe("source-preserving evidence redaction", () => {
});
});

it("redacts initial values from sensitive textarea and select controls", () => {
const text =
'<textarea name="password">BSK_SYNTHETIC_SECRET_7264</textarea>' +
'<select name="token"><option value="BSK_SYNTHETIC_SECRET_7264">Credential A</option></select>' +
'<textarea name="nickname">Alice</textarea>' +
'<select name="country"><option value="cn">China</option></select>';

expect(redactBody(text, "text/html")).toEqual({
text:
'<textarea data-bsk-redacted="true"></textarea>' +
'<select data-bsk-redacted="true"></select>' +
'<textarea name="nickname">Alice</textarea>' +
'<select name="country"><option value="cn">China</option></select>',
redacted: true,
truncated: false,
replay_safe: false,
});
});

it("redacts encoded metadata and unclosed sensitive controls", () => {
const textarea = redactBody(
'<textarea name="pass&#119;ord">BSK_SYNTHETIC_SECRET_7264<p>rest of response</p>',
"text/html",
);
expect(textarea.text).toBe('<textarea data-bsk-redacted="true">');
expect(textarea).toMatchObject({ redacted: true, truncated: false, replay_safe: false });

expect(
redactBody(
'<select autocomplete="section-login one-time-code"><option value="BSK_SYNTHETIC_SECRET_7264">Code</option></select>',
"text/html",
).text,
).toBe('<select data-bsk-redacted="true"></select>');
});

it.each([
'name="nick&#110;ame"',
'name="display&period;name"',
Expand Down
69 changes: 44 additions & 25 deletions apps/extension/src/debug/redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,49 @@ export function redactHeaders(value: unknown): Record<string, string> {
return redactHeaderEvidence(value).headers;
}

function sensitiveHtmlAttributes(tag: string): boolean {
const attributes = tag.matchAll(
/\b(type|name|id|autocomplete)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi,
);
for (const match of attributes) {
const value = htmlField(match[2] ?? match[3] ?? match[4]);
if (
secretField(value) ||
secretField(value.toLowerCase()) ||
value
.trim()
.split(/\s+/)
.some((token) => /^(?:current-password|new-password|one-time-code|cc-.+)$/i.test(token))
) {
return true;
}
}
return false;
}

function redactSensitiveHtmlControls(text: string): string {
// Remove server-rendered values from controls whose field metadata marks them
// as sensitive. Inputs have no child content; textareas and selects do, so
// remove the whole control to avoid retaining either text content or option
// values that would be submitted as the initial value.
let result = text.replace(/<input\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi, (tag) =>
sensitiveHtmlAttributes(tag) ? '<input data-bsk-redacted="true">' : tag,
);
for (const element of ["textarea", "select"] as const) {
const block = new RegExp(
`<${element}\\b(?:[^>"']|"[^"]*"|'[^']*')*>[\\s\\S]*?(<\\/${element}\\s*>|$)`,
"gi",
);
result = result.replace(block, (value, closing: string) => {
const end = value.indexOf(">") + 1;
const opening = value.slice(0, end);
if (!sensitiveHtmlAttributes(opening)) return value;
return `<${element} data-bsk-redacted="true">${closing ? `</${element}>` : ""}`;
});
}
return result;
}

function redactJson(text: string, bounds: { truncated: boolean }): string {
const root = parseJsonSource(text);
const parts: string[] = [];
Expand Down Expand Up @@ -187,31 +230,7 @@ export function redactBody(
};
}
} else if (/html/i.test(mime)) {
// A password can also occur in a server-rendered input's value attribute.
// Remove that input from retained HTML rather than exposing its initial value.
result = redactText(
text.replace(/<input\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi, (tag) => {
const attributes = tag.matchAll(
/\b(type|name|id|autocomplete)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi,
);
for (const match of attributes) {
const value = htmlField(match[2] ?? match[3] ?? match[4]);
if (
secretField(value) ||
secretField(value.toLowerCase()) ||
value
.trim()
.split(/\s+/)
.some((token) =>
/^(?:current-password|new-password|one-time-code|cc-.+)$/i.test(token),
)
)
return '<input data-bsk-redacted="true">';
}
return tag;
}),
BODY_CHARS,
);
result = redactText(redactSensitiveHtmlControls(text), BODY_CHARS);
} else if (/x-www-form-urlencoded/i.test(mime)) {
// Preserve untouched bytes, ordering and duplicates (including signed form bodies).
result = text
Expand Down
Loading