Known Limitations Community
This document describes accepted limitations in the current release. Items marked [Resolved] have been fixed but are kept for historical reference.
1. Dashboard Content Security Policy allows inline scripts [Resolved]
Section titled “1. Dashboard Content Security Policy allows inline scripts [Resolved]”Status: Fixed
Inline JavaScript has been extracted to static/dashboard.js. CSP is now
script-src 'self' with no unsafe-inline or unsafe-eval. This was
completed as part of the frontend auth hardening epic.
2. Dashboard authentication uses browser sessionStorage [Resolved]
Section titled “2. Dashboard authentication uses browser sessionStorage [Resolved]”Status: Fixed
Dashboard now uses HttpOnly + SameSite=Strict session cookies via POST /auth/login.
The admin API key is no longer stored in browser storage. WebSocket auth uses
cookie-based session validation as primary method.
3. DataGuard violation terminates the connection permanently
Section titled “3. DataGuard violation terminates the connection permanently”Status: Accepted risk (intentional security design)
When a single query response or the rolling window transfer exceeds the configured DataGuard limit, the connection is flagged as violated. All subsequent queries on that connection will be rejected with a PostgreSQL ErrorResponse:
FATAL: Querycop: data guard: response size NNN bytes exceeds limit NNN bytes (reconnect to continue)Rationale: This prevents data exfiltration via repeated queries within a single connection. An attacker who triggers one large response cannot continue to query on the same connection.
Impact: Legitimate large query results (e.g., analytics exports) that exceed the per-query limit will terminate the connection. The client application must reconnect.
Configuration:
GATEKEEPER_MAX_RESPONSE_MB: Per-query response limit (default: 100 MB)GATEKEEPER_MAX_WINDOW_MB: Rolling 60-second transfer limit (default: 500 MB)
Workaround: Increase limits for trusted users/applications. Use pagination for large result sets. Monitor DataGuard violation events via audit log or WebSocket.
4. AI risk scoring is advisory and subject to prompt injection
Section titled “4. AI risk scoring is advisory and subject to prompt injection”Status: Accepted risk (inherent limitation of LLM-based analysis)
SQL queries are sent to an LLM for risk scoring. The system includes multiple layers of defense:
- Comment stripping: SQL comments are removed before LLM submission
- Input sanitization: Control characters and known injection patterns are normalized
- Server-side score override: Destructive keywords (DELETE, DROP, TRUNCATE) with suspiciously low AI scores are automatically overridden to score >= 50
- System prompt hardening: Anti-injection instructions in the system prompt
- Threshold-based approval: Auto-approval decisions are based on numeric score thresholds, not on AI text recommendations
Impact: A crafted SQL query may be able to influence the AI’s reason text
(displayed in Slack/dashboard), but cannot bypass server-side score enforcement.
The reason field should be treated as untrusted advisory text.
Workaround: Set conservative auto-approval thresholds. Require human
approval for all destructive queries (auto_approve_threshold: 0). Monitor AI
score distributions for anomalies.
5. Data masking limitations
Section titled “5. Data masking limitations”Status: Known limitation
- Binary format columns: Columns returned in binary format (FormatCode=1) are not masked. Text format (FormatCode=0) is the default for most PostgreSQL clients.
- Table OID resolution: Masking rules match by column name only. Table OID to table name resolution is not implemented.
- Extended query protocol: Parse/Bind/Execute messages are tracked for semantic state, but masking applies only to RowDescription/DataRow messages which are the same regardless of simple vs extended query path.
6. Extended query protocol coverage
Section titled “6. Extended query protocol coverage”Status: Known limitation
- Parse, Bind, Execute, Describe, Close, and Sync messages are tracked
- Statement name → SQL text mapping is maintained per connection
- Portal → statement resolution is supported
- Not covered: COPY protocol, function calls, cursors with FETCH
- Not covered: Full SQL semantic analysis of parameterized queries
7. MySQL protocol coverage
Section titled “7. MySQL protocol coverage”Status: Known limitation
- MySQL text protocol (COM_QUERY) is supported with handshake, user extraction, and query classification/approval
- Not covered: MySQL binary protocol (COM_STMT_PREPARE, COM_STMT_EXECUTE)
- Not covered: MySQL SSL/TLS upgrade
- Not covered: MySQL data masking (RowDescription/DataRow is PostgreSQL-specific)
- Not covered: COM_CHANGE_USER, COM_RESET_CONNECTION
- MySQL auth is relayed to backend; proxy does not perform auth itself
8. Distributed approval limitations
Section titled “8. Distributed approval limitations”Status: Known limitation
- Cross-node approval uses Redis Pub/Sub for completion signaling
- The origin node (where the query is blocked) must remain connected to Redis for the duration of the approval wait
- If Redis goes down during the wait, the pending request will time out
- WebSocket events are not broadcast across nodes
8. Deprecated rename aliases (Querycop transition)
Section titled “8. Deprecated rename aliases (Querycop transition)”Status: Accepted (deprecation period)
The product was renamed from QueryGuard to Querycop in Phase B3. The Go module path, user-facing docs, and LP have been fully migrated, but several wire-level identifiers still accept the old name for one release cycle:
GATEKEEPER_REDIS_KEY_PREFIX(env) — deprecated alias forGATEKEEPER_CLUSTER_KEY_PREFIX. Still read at startup, scheduled for removal in the next major version.- Slack interaction handler — accepts both
querycop_approve/querycop_reject(new) andqueryguard_approve/queryguard_reject(legacy) inaction_idvalues. Messages sent by the notifier now use the new IDs; the legacy IDs are only kept to support in-flight messages posted before the upgrade. - Helm chart directory — renamed
charts/queryguard/→charts/querycop/. A tombstone README remains at the old path. The chart’snameOverridevalue keeps in-placehelm upgradeof pre-rename releases safe (--set nameOverride=queryguard); seedocs/configuration.md§9.4 for the migration steps. - Environment variables prefixed
GATEKEEPER_*and binary namegatekeeper— intentionally retained (see CLAUDE.md). No deprecation planned.
See docs/configuration.md section 9 for full migration guidance.
9. SQL parsing / classification limitations
Section titled “9. SQL parsing / classification limitations”Status: Known limitation (intentional fail-safe design)
Querycop classifies query intent with a lightweight, protocol-independent text
parser (pkg/sqlparse), not a full SQL grammar. It strips comments, splits
stacked statements on ; (respecting single-quote strings, dollar-quote bodies,
and comments), and matches keywords on word boundaries. The classifier is a
firewall input, so every approximation is biased toward over-classify
(treat as more destructive) and never under-classify (downgrade a
destructive statement to a lighter action).
- Single-quote string literals: keyword text inside
'...'literals is data and is blanked before classification, soSELECT 'please DROP everything'classifies as a read, not DDL. An unterminated literal is not blanked — its keywords stay visible (fail-safe over-classify). - Dollar-quote bodies are not data-blanked: a
DO $$ ... $$/$tag$ ... $tag$body can be executed server-side, so keywords inside it still drive classification (DO $$ ... DELETE ... $$classifies as a delete). - Nested block comments are a cross-protocol trade-off: PostgreSQL nests
/* ... */; MySQL/MariaDB do not (they close the comment at the first*/). The stripper deliberately stops at the first*/. This keeps it fail-safe for MySQL (a nesting-aware stripper would hide MySQL-executable code that appears after the first*/= bypass). The documented consequence: a keyword positioned between an inner*/and an outer*/(e.g./* outer /* inner */ DROP TABLE t */ SELECT 1) is treated as live code and over-classifies on PostgreSQL (where it is really still commented out), while being classified correctly on MySQL (where it really executes). A keyword that is before the first*/is genuinely inside the comment on both engines and is stripped normally. - MySQL executable comments:
/*! ... */and/*!NNNNN ... */bodies are executed by MySQL, so they are preserved verbatim (not stripped) and their keywords drive classification. - No semantic analysis: keyword matching does not understand full SQL semantics (e.g. a destructive statement disguised via a stored-procedure call or a non-keyworded mechanism may not be recognized). Combine the classifier with RBAC policy and AI risk scoring rather than relying on it alone.
Change History
Section titled “Change History”| Date | Change |
|---|---|
| 2026-04-01 | Initial known limitations document |
| 2026-04-04 | Added masking and extended query limitations |
| 2026-04-04 | Added MySQL and distributed approval limitations |
| 2026-04-21 | Documented QueryGuard → Querycop rename deprecations |
| 2026-06-18 | Added SQL parsing / classification limitations (string-literal blanking, nested-comment cross-protocol fail-safe, tagged dollar quotes) |