Files
chrome-use/src/action-policy.test.ts
T
Chris Tate bc1e917e87 add security hardening features (#543)
* add security hardening features

- Add authentication vault (`auth save/login/list/show/delete`) so credentials are stored locally and never exposed to the LLM (fixes Snyk W007)
- Add `--content-boundaries` flag to wrap page-sourced output in structural markers, helping LLMs distinguish tool output from untrusted page content (fixes Snyk W011)
- Add `--allowed-domains` flag to restrict browser navigation to trusted domains
- Add `--action-policy` for static allow/deny gating of action categories, with opt-in `--confirm-actions`/`--confirm-interactive` for orchestrator or human-in-the-loop confirmation
- Add `--max-output` flag to truncate large page outputs, preventing context flooding
- New docs page at /security, updated README, SKILL.md, CLI help text, and templates

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* docs
2026-02-25 15:33:20 -06:00

214 lines
8.3 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
getActionCategory,
checkPolicy,
loadPolicyFile,
describeAction,
KNOWN_CATEGORIES,
type ActionPolicy,
} from './action-policy.js';
describe('action-policy', () => {
describe('getActionCategory', () => {
it('should return correct category for known actions', () => {
expect(getActionCategory('navigate')).toBe('navigate');
expect(getActionCategory('click')).toBe('click');
expect(getActionCategory('fill')).toBe('fill');
expect(getActionCategory('evaluate')).toBe('eval');
expect(getActionCategory('download')).toBe('download');
expect(getActionCategory('upload')).toBe('upload');
expect(getActionCategory('snapshot')).toBe('snapshot');
expect(getActionCategory('scroll')).toBe('scroll');
expect(getActionCategory('wait')).toBe('wait');
expect(getActionCategory('gettext')).toBe('get');
expect(getActionCategory('route')).toBe('network');
expect(getActionCategory('state_save')).toBe('state');
expect(getActionCategory('hover')).toBe('interact');
});
it('should return _internal for internal actions', () => {
expect(getActionCategory('launch')).toBe('_internal');
expect(getActionCategory('close')).toBe('_internal');
expect(getActionCategory('session')).toBe('_internal');
expect(getActionCategory('auth_save')).toBe('_internal');
expect(getActionCategory('confirm')).toBe('_internal');
});
it('should return eval for security-sensitive actions', () => {
expect(getActionCategory('setcontent')).toBe('eval');
expect(getActionCategory('expose')).toBe('eval');
expect(getActionCategory('addstyle')).toBe('eval');
});
it('should return unknown for unrecognized actions', () => {
expect(getActionCategory('nonexistent')).toBe('unknown');
expect(getActionCategory('')).toBe('unknown');
});
it('should return get for semantic locator actions', () => {
expect(getActionCategory('getbyrole')).toBe('get');
expect(getActionCategory('getbytext')).toBe('get');
expect(getActionCategory('getbylabel')).toBe('get');
});
});
describe('checkPolicy', () => {
it('should always allow internal actions regardless of policy', () => {
const denyAll: ActionPolicy = { default: 'deny' };
expect(checkPolicy('launch', denyAll, new Set())).toBe('allow');
expect(checkPolicy('close', denyAll, new Set())).toBe('allow');
expect(checkPolicy('session', denyAll, new Set())).toBe('allow');
});
it('should allow all when no policy and no confirm categories', () => {
expect(checkPolicy('navigate', null, new Set())).toBe('allow');
expect(checkPolicy('click', null, new Set())).toBe('allow');
expect(checkPolicy('evaluate', null, new Set())).toBe('allow');
});
it('should deny actions in explicit deny list', () => {
const policy: ActionPolicy = { default: 'allow', deny: ['eval', 'download'] };
expect(checkPolicy('evaluate', policy, new Set())).toBe('deny');
expect(checkPolicy('download', policy, new Set())).toBe('deny');
expect(checkPolicy('click', policy, new Set())).toBe('allow');
});
it('should allow actions in explicit allow list with deny default', () => {
const policy: ActionPolicy = { default: 'deny', allow: ['navigate', 'snapshot'] };
expect(checkPolicy('navigate', policy, new Set())).toBe('allow');
expect(checkPolicy('snapshot', policy, new Set())).toBe('allow');
expect(checkPolicy('click', policy, new Set())).toBe('deny');
});
it('should return confirm for actions in confirm categories', () => {
expect(checkPolicy('evaluate', null, new Set(['eval']))).toBe('confirm');
expect(checkPolicy('download', null, new Set(['download']))).toBe('confirm');
});
it('should deny over confirm when action is in deny list', () => {
const policy: ActionPolicy = { default: 'allow', deny: ['eval'] };
expect(checkPolicy('evaluate', policy, new Set(['eval']))).toBe('deny');
});
it('should use default policy for unknown categories', () => {
const denyPolicy: ActionPolicy = { default: 'deny' };
const allowPolicy: ActionPolicy = { default: 'allow' };
expect(checkPolicy('nonexistent', denyPolicy, new Set())).toBe('deny');
expect(checkPolicy('nonexistent', allowPolicy, new Set())).toBe('allow');
});
});
describe('loadPolicyFile', () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'action-policy-test-'));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('should load a valid allow-default policy', () => {
const policyPath = path.join(tempDir, 'policy.json');
fs.writeFileSync(policyPath, JSON.stringify({ default: 'allow', deny: ['eval'] }));
const policy = loadPolicyFile(policyPath);
expect(policy.default).toBe('allow');
expect(policy.deny).toEqual(['eval']);
});
it('should load a valid deny-default policy', () => {
const policyPath = path.join(tempDir, 'policy.json');
fs.writeFileSync(
policyPath,
JSON.stringify({ default: 'deny', allow: ['navigate', 'snapshot'] })
);
const policy = loadPolicyFile(policyPath);
expect(policy.default).toBe('deny');
expect(policy.allow).toEqual(['navigate', 'snapshot']);
});
it('should throw on invalid default value', () => {
const policyPath = path.join(tempDir, 'policy.json');
fs.writeFileSync(policyPath, JSON.stringify({ default: 'maybe' }));
expect(() => loadPolicyFile(policyPath)).toThrow('must be "allow" or "deny"');
});
it('should throw on missing file', () => {
expect(() => loadPolicyFile(path.join(tempDir, 'missing.json'))).toThrow();
});
it('should throw on invalid JSON', () => {
const policyPath = path.join(tempDir, 'policy.json');
fs.writeFileSync(policyPath, 'not json');
expect(() => loadPolicyFile(policyPath)).toThrow();
});
it('should warn on unrecognized category names', () => {
const policyPath = path.join(tempDir, 'policy.json');
fs.writeFileSync(
policyPath,
JSON.stringify({ default: 'allow', deny: ['eval', 'typo_category'] })
);
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const policy = loadPolicyFile(policyPath);
expect(policy.default).toBe('allow');
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('unrecognized action category "typo_category"')
);
warnSpy.mockRestore();
});
it('should not warn on valid category names', () => {
const policyPath = path.join(tempDir, 'policy.json');
fs.writeFileSync(
policyPath,
JSON.stringify({ default: 'deny', allow: ['navigate', 'snapshot', 'get'] })
);
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
loadPolicyFile(policyPath);
expect(warnSpy).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
});
describe('describeAction', () => {
it('should describe navigate actions', () => {
expect(describeAction('navigate', { url: 'https://example.com' })).toBe(
'Navigate to https://example.com'
);
});
it('should describe eval actions with truncation', () => {
const longScript = 'a'.repeat(200);
const desc = describeAction('evaluate', { script: longScript });
expect(desc).toContain('Evaluate JavaScript:');
expect(desc.length).toBeLessThan(200);
});
it('should describe click actions', () => {
expect(describeAction('click', { selector: '#btn' })).toBe('Click #btn');
});
it('should describe dblclick actions', () => {
expect(describeAction('dblclick', { selector: '#btn' })).toBe('Double-click #btn');
});
it('should describe tap actions', () => {
expect(describeAction('tap', { selector: '#btn' })).toBe('Tap #btn');
});
it('should describe fill actions', () => {
expect(describeAction('fill', { selector: '#input' })).toBe('Fill #input');
});
it('should use fallback for unknown actions', () => {
const desc = describeAction('scroll', {});
expect(desc).toContain('scroll');
});
});
});