Repository navigation
fix: harden query-database — block sensitive columns + length cap (closes #166, #129) - #199
Merged
Merged
Conversation
Two layers of defense for the query-database ability, which executes LLM-generated SQL under the manage_options cap. #166 — Email/password reconnaissance: Previously the user could send WHERE user_email='[email protected]' and infer existence from the row count, bypassing the [REDACTED] field display. Now any query that mentions user_pass / user_email / user_activation_key / session_tokens — anywhere in the string, not just in SELECT lists — is rejected before execution. The blocklist lives in a shared wp_agentic_admin_sensitive_columns() helper so the same list drives both query rejection and result-row redaction. #129 — Unescaped parameter + missing safety annotations: $wpdb->prepare() can't be used here because the LLM authors the whole query (no separable placeholders to bind). Documented why, alongside all the other gates that defend the call: - read-only verb gate (SELECT/SHOW/DESCRIBE/EXPLAIN only) - forbidden-keyword blocklist (INSERT/UPDATE/DELETE/DROP/…) - sensitive-column blocklist (issue #166) - 2000-char length cap (new — prevents pathological obfuscation) - manage_options capability check - per-row redaction as belt-and-suspenders for SELECT * Consolidated the existing single-line phpcs:ignore into one covering WordPress.DB.PreparedSQL.NotPrepared + .DirectDatabaseQuery.DirectQuery + .DirectDatabaseQuery.NoCaching, with the rationale comment directly above the call. Caching is intentionally skipped: each LLM-generated query is unique, so a cache would never hit while doubling our DB call surface. Renames: wp_agentic_admin_is_safe_query() → wp_agentic_admin_check_query_safety() Returns string|true instead of bool so the caller can surface the specific reason a query was rejected (helps the LLM correct course on the next turn). New helpers: wp_agentic_admin_sensitive_columns() — single source of truth wp_agentic_admin_redact_sensitive_row($row) — result-row redactor WP_AGENTIC_ADMIN_QUERY_MAX_LENGTH constant — 2000 Verified manually via wp_cli eval in the running Playground: - SELECT * FROM wp_users WHERE user_email = '…' → blocked - SELECT user_pass FROM wp_users → blocked - SELECT ID, user_login FROM wp_users → passed - DELETE FROM wp_users → blocked - 2500-char SELECT string → blocked - INSERT INTO wp_users VALUES (1) → blocked PHP lint: clean (all phpcs warnings suppressed with rationale). JS tests: 96 passing, unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
5 tasks
ivdimova
approved these changes
May 12, 2026
ivdimova
left a comment
Collaborator
There was a problem hiding this comment.
Solid two-layer defence: sensitive columns blocked at query parse time, then redacted in results as belt-and-suspenders for SELECT *. Row-count side-channel reasoning is correct. Translated error strings are appropriate for wp.org. Safe to merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First of three security PRs for v0.11. Two real-world attack vectors closed.
Net: +99 / -16 lines, 1 file touched.
Closes
#166 — Email/password reconnaissance via WHERE clauses
The query-database ability runs LLM-authored SQL. Previously, sensitive column values like `user_pass` and `user_email` were redacted in the response display, but a user could still send:
```sql
SELECT * FROM wp_users WHERE user_email = '[email protected]'
```
An empty result vs. one-row result leaks whether that email exists — confirming users one address at a time.
Fix: Reject any query that mentions a sensitive column anywhere in the string, before execution. The blocklist lives in a shared `wp_agentic_admin_sensitive_columns()` helper:
```php
function wp_agentic_admin_sensitive_columns(): array {
return [ 'user_pass', 'user_email', 'user_activation_key', 'session_tokens' ];
}
```
Plus a belt-and-suspenders `wp_agentic_admin_redact_sensitive_row()` that scrubs those columns from result rows, so `SELECT *` doesn't smuggle them out.
#129 — Unescaped parameter + missing safety rationale
`$wpdb->prepare()` can't be used here because the LLM authors the whole query (no separable placeholders to bind). The phpcs warnings (`PreparedSQL.NotPrepared`, `DirectDatabaseQuery.DirectQuery`, `DirectDatabaseQuery.NoCaching`) are intentional, but were inadequately documented. Now a single `phpcs:ignore` covers all three with a rationale comment listing every defense layer.
Also added a 2000-char length cap to prevent pathological queries that try to bypass the keyword blocklist via obfuscation.
What changed
Verification
Manual smoke test via `wp_cli eval` in the running Playground:
What's next
Two more security PRs to land before WP.org submission:
Test plan
🤖 Generated with Claude Code