Skip to content

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:

  1. Comment stripping: SQL comments are removed before LLM submission
  2. Input sanitization: Control characters and known injection patterns are normalized
  3. Server-side score override: Destructive keywords (DELETE, DROP, TRUNCATE) with suspiciously low AI scores are automatically overridden to score >= 50
  4. System prompt hardening: Anti-injection instructions in the system prompt
  5. 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.


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.

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

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

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 for GATEKEEPER_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) and queryguard_approve / queryguard_reject (legacy) in action_id values. 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’s nameOverride value keeps in-place helm upgrade of pre-rename releases safe (--set nameOverride=queryguard); see docs/configuration.md §9.4 for the migration steps.
  • Environment variables prefixed GATEKEEPER_* and binary name gatekeeper — 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, so SELECT '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.

DateChange
2026-04-01Initial known limitations document
2026-04-04Added masking and extended query limitations
2026-04-04Added MySQL and distributed approval limitations
2026-04-21Documented QueryGuard → Querycop rename deprecations
2026-06-18Added SQL parsing / classification limitations (string-literal blanking, nested-comment cross-protocol fail-safe, tagged dollar quotes)