Skip to content

fix: close SQL-injection and secrets gaps found by a security audit - #4

Merged
SHSharkar merged 1 commit into
mainfrom
sazzad/harden-and-modernize-ownpg
Sep 13, 2026
Merged

SHSharkar merged 1 commit into
mainfrom
sazzad/harden-and-modernize-ownpg

Conversation

@SHSharkar

Copy link
Copy Markdown
Contributor

Summary

A security and protocol-conformance audit of ownpg-core found a SQL-injection-class bypass in
two DDL tools, two secret files loaded with no permission check, and a rate-limited HTTP response
that violated the MCP wire schema. This PR fixes all of them, plus the gaps a live conformance run
against the fixed server then surfaced.

Motivation

Statement classification is the security boundary this whole project depends on: every argument
that becomes part of a SQL statement is supposed to pass through it before running. EXCLUDE
constraint elements and ATTACH PARTITION bounds did not. SSH private keys and TLS client keys were
read with no permission check, unlike every other secret file in the config layer. Running the
real @modelcontextprotocol/conformance suite against a live server then found a wire-schema
violation (a rate-limited response with id: null) and a missing readiness-only rate limit.

Changes

  • Added operator, partition_bound, and returns_type to render.rs, three validators that
    probe an untrusted fragment through the classifier, matching the existing
    type_name/expression pattern.
  • Routed EXCLUDE constraint elements and ATTACH PARTITION bounds (table.rs) through those
    validators instead of interpolating them raw.
  • Added a permission check (refuse_open_permissions) before connect/ssh.rs and connect/tls.rs
    read a private key file.
  • Extended the Windows permission warning in config/resolve.rs to cover sslkey, the SSH key
    file, and the HTTP state key file.
  • Fixed a rate-limited HTTP response carrying id: null, which the 2026-07-28 MCP wire schema
    forbids.
  • Added a readiness-only rate limiter so /healthz/ready no longer shares a bucket with real tool
    calls.
  • Raised the default HTTP rate limit and connection cap from fixed low defaults to unlimited; both
    stay configurable by environment variable, and the rate-limit mechanism itself keeps live test
    coverage pinned to a low limit.
  • Declared an empty extensions capability in the server's initialize response, closing a
    wire-format gap the conformance suite flagged.
  • Renamed classify::check to classify::authorize, CopyShape to CopyDetails, and several
    other internal names the codebase's own naming pass flagged; classify::authorize is a public
    rename.
  • Corrected README, CHANGELOG, SECURITY.md, and the CLAUDE.md/AGENTS.md pair against the real
    source and the repository's current public visibility.

Type of Change

  • Bug fix (fix)
  • Breaking change (append ! to type)

Testing

Test type:

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed

Steps to verify:

  1. cargo nextest run --workspace --all-features --locked with OWNPG_TEST_DSN set against a
    local PostgreSQL: 296 tests pass.
  2. cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features --locked -- -D warnings, cargo doc, cargo machete, cargo audit, cargo deny check: all pass clean.
  3. npx @modelcontextprotocol/conformance server --requirements 2026-07-28 against a live local
    build: baseline check passes, every remaining failure is a documented, verified gap in
    .github/conformance/expected-failures.yaml.

Breaking Changes

What breaks: classify::authorize replaces the public function classify::check in
ownpg-core. Nothing outside this crate calls it today.
Migration: rename the call site from classify::check to classify::authorize; the signature
is unchanged.

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests added/updated
  • Documentation updated (if applicable)
  • No new warnings or errors
  • Build passes locally
  • PR title follows Conventional Commits

Why
A round of security and protocol-conformance audits found real gaps:
EXCLUDE constraint elements and ATTACH PARTITION bounds went straight
into SQL without passing through the classifier, the same class of
bypass that hit a competing Postgres MCP server; SSH private keys and
TLS client keys loaded with no permission check, unlike every other
secret file; a rate-limited HTTP response carried a JSON-RPC id of
null, which the 2026-07-28 wire schema forbids; and the health-check
endpoint shared its rate-limit bucket with real traffic instead of
having its own.

What changed
render.rs gained operator, partition_bound, and returns_type, three
validators that probe a fragment through the classifier the same way
type_name and expression already do; table.rs, routine.rs, and
index.rs now route every untrusted fragment through one of them.
connect/ssh.rs and connect/tls.rs now call refuse_open_permissions
before reading a key file, matching the existing profile and token
file checks. config/resolve.rs extends the Windows permission warning
to sslkey, the SSH key file, and the HTTP state key file. The HTTP
rate limiter's default moved from 60 calls per minute to unlimited,
since the fixed default was throttling normal agent traffic; the
mechanism itself still has test coverage, pinned to a low limit for
that one test. A readiness-only rate limiter was added so health
checks no longer share a bucket with tool calls. classify::check,
CopyShape, and a handful of other internal names were renamed for
clarity; classify::check becomes classify::authorize, a public
rename flagged for review since ownpg-core's API is semver-checked.
README, CHANGELOG, SECURITY, and the CLAUDE.md/AGENTS.md pair were
corrected against the real source and against the repository's
current public visibility.

Risk
classify::authorize is a public rename; nothing outside this crate
calls it today, but it is a breaking change to ownpg-core's API.
@SHSharkar SHSharkar added the bug Something isn't working label Sep 13, 2026
@SHSharkar SHSharkar self-assigned this Sep 13, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden SQL rendering, secret loading, and MCP HTTP conformance

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Validates all dynamic DDL fragments through PostgreSQL classification before execution.
• Enforces private-key permissions and hardens MCP HTTP limits, responses, and capabilities.
• Expands conformance, integration, property testing, release safety, and user documentation.
Diagram

graph TD
  X["External Inputs"] --> V["DDL Validators"] --> C["SQL Classifier"] --> R["Protected Runtime"]
  X --> P["Permission Checks"] --> S["Secure Connectors"] --> R
  X --> G["HTTP Gatekeeper"] --> R
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Replace fragments with typed DDL fields
  • ➕ Eliminates most free-form SQL fragments at the tool boundary.
  • ➕ Makes invalid combinations rejectable before SQL rendering.
  • ➖ Would break existing tool arguments and the public protocol surface.
  • ➖ Cannot easily represent PostgreSQL's full operator, bound, and type syntax.
  • ➖ Would substantially expand the DDL model and maintenance burden.

Recommendation: Keep the PR's classifier-probe approach. It closes the immediate bypasses while preserving PostgreSQL syntax coverage and existing tool contracts, and it reuses the project's established security boundary instead of introducing weaker string-pattern checks. Typed arguments could be introduced incrementally where PostgreSQL syntax is naturally bounded.

Files changed (28) +529 / -203

Bug fix (9) +297 / -126
resolve.rsWarn about every unchecked Windows secret file +6/-1

Warn about every unchecked Windows secret file

• Extends Windows permission warnings to TLS client keys, SSH private keys, and HTTP state-key files.

crates/ownpg-core/src/config/resolve.rs

ssh.rsReject unsafe SSH private-key permissions +15/-13

Reject unsafe SSH private-key permissions

• Checks private-key file permissions before parsing key material and preserves passphrase fallback behavior. It also renames the SSH agent connection helper for clarity.

crates/ownpg-core/src/connect/ssh.rs

tls.rsReject unsafe TLS client-key permissions +2/-0

Reject unsafe TLS client-key permissions

• Runs the shared secret-file permission check before reading a TLS client private key.

crates/ownpg-core/src/connect/tls.rs

render.rsAdd classifier-backed DDL fragment validators +96/-0

Add classifier-backed DDL fragment validators

• Adds validators for operators, partition bounds, and function return types. Each fragment is embedded in a constrained probe statement and rejected when parsing, classification, object scope, or refusal checks fail.

crates/ownpg-core/src/render.rs

mod.rsHarden MCP HTTP limiting and response conformance +70/-47

Harden MCP HTTP limiting and response conformance

• Introduces a dedicated readiness limiter, emits a schema-valid numeric JSON-RPC ID for rate-limit errors, and centralizes gatekeeper construction. It also applies body timeouts consistently, sweeps both limiter stores, and tests the response ID contract.

crates/ownpg-core/src/server/http/mod.rs

mod.rsAdvertise extensions and offload audit writes +28/-21

Advertise extensions and offload audit writes

• Adds an empty MCP extensions capability and moves blocking audit writes onto blocking worker tasks. Internal protocol-request and tool-call recording methods are renamed for clarity.

crates/ownpg-core/src/server/mod.rs

confirm.rsRe-request missing destructive-operation confirmations +46/-39

Re-request missing destructive-operation confirmations

• Extracts confirmation-request construction into a shared helper and reissues the request when a client omits its response instead of immediately failing.

crates/ownpg-core/src/tools/confirm.rs

routine.rsValidate routine return types before rendering +7/-2

Validate routine return types before rendering

• Routes function return-type fragments through the new classifier-backed validator instead of interpolating them directly.

crates/ownpg-core/src/tools/ddl/routine.rs

table.rsValidate partition bounds and EXCLUDE elements +27/-3

Validate partition bounds and EXCLUDE elements

• Validates ATTACH PARTITION bounds through the classifier and decomposes each EXCLUDE element into a validated index expression and operator. This closes direct SQL-fragment interpolation paths.

crates/ownpg-core/src/tools/ddl/table.rs

Refactor (9) +53 / -49
classify.rsRename classification authorization APIs +14/-10

Rename classification authorization APIs

• Renames the public 'check' function to 'authorize' and 'CopyShape' to 'CopyDetails', updating classifier tests accordingly. The public function rename is a breaking API change.

crates/ownpg-core/src/classify.rs

engine.rsClarify transaction and COPY helper names +13/-13

Clarify transaction and COPY helper names

• Renames the savepoint constant and internal COPY and transaction-settlement helpers without changing execution behavior.

crates/ownpg-core/src/engine.rs

limit.rsClarify limiter rate field naming +4/-4

Clarify limiter rate field naming

• Renames the internal call-count field to 'calls_per_minute' and updates accessors and debug output.

crates/ownpg-core/src/server/http/limit.rs

resources.rsClarify resource URI target naming +8/-8

Clarify resource URI target naming

• Renames the generic 'Target' enum to 'ResourceTarget' throughout URI parsing, resource dispatch, and tests.

crates/ownpg-core/src/server/resources.rs

stdio.rsClarify stdio line tracking and shutdown naming +8/-8

Clarify stdio line tracking and shutdown naming

• Renames the current-line byte counter and signal-waiting helper to describe their responsibilities more precisely.

crates/ownpg-core/src/server/stdio.rs

index.rsShare index-element validation with constraints +1/-1

Share index-element validation with constraints

• Exposes the index-element renderer within the DDL module so EXCLUDE constraints can reuse its validation.

crates/ownpg-core/src/tools/ddl/index.rs

types.rsClarify available-extension result naming +3/-3

Clarify available-extension result naming

• Renames a local extension-list result variable and updates structured output and audit row-count references.

crates/ownpg-core/src/tools/ddl/types.rs

read.rsAdopt the renamed SQL authorization API +1/-1

Adopt the renamed SQL authorization API

• Updates read-tool statement checks to call 'classify::authorize'.

crates/ownpg-core/src/tools/read.rs

serve.rsUse the Gatekeeper constructor API +1/-1

Use the Gatekeeper constructor API

• Updates the CLI HTTP server startup path to construct gatekeepers through 'Gatekeeper::build'.

crates/ownpg/src/serve.rs

Tests (3) +87 / -13
expected-failures.yamlRefresh MCP conformance expectations +12/-9

Refresh MCP conformance expectations

• Removes expectations for scenarios that now pass and records remaining JSON Schema, custom-header, task, and missing-response gaps.

.github/conformance/expected-failures.yaml

shape.rsProperty-test Unicode sanitization +20/-0

Property-test Unicode sanitization

• Adds high-volume property tests proving sanitization removes invisible characters and does not panic on arbitrary Unicode.

crates/ownpg-core/src/shape.rs

http.rsExpand live HTTP compatibility and limiter coverage +55/-4

Expand live HTTP compatibility and limiter coverage

• Pins rate-sensitive tests to an explicit low limit after defaults became unlimited. Adds coverage proving legacy GET and DELETE session methods are accepted when older-client compatibility is enabled.

crates/ownpg-core/tests/live/http.rs

Documentation (5) +85 / -11
AGENTS.mdCorrect house-rule scope documentation +1/-1

Correct house-rule scope documentation

• Clarifies that em-dash checks include root license files while borrowed-code checks exclude them.

AGENTS.md

CHANGELOG.mdCorrect automatic and optional tool-group documentation +1/-1

Correct automatic and optional tool-group documentation

• Documents that write and transaction tools load automatically when the selected access mode permits writes.

CHANGELOG.md

CLAUDE.mdCorrect house-rule scope documentation +1/-1

Correct house-rule scope documentation

• Keeps contributor guidance aligned with the actual CI scopes for em-dash and borrowed-code checks.

CLAUDE.md

README.mdExpand client connection and authentication guidance +77/-3

Expand client connection and authentication guidance

• Adds navigation, Claude Code and Desktop examples, Streamable HTTP authentication guidance, and OAuth discovery details. It also corrects password, tool-group, and contribution instructions.

README.md

SECURITY.mdAlign vulnerability reporting and security scope +5/-5

Align vulnerability reporting and security scope

• Updates repository visibility language, adds an email fallback, clarifies response expectations, and documents rds_superuser and static bearer-token behavior.

SECURITY.md

Other (2) +7 / -4
release.ymlVerify the rustup installer before release builds +4/-1

Verify the rustup installer before release builds

• Downloads a pinned rustup installer script and verifies its SHA-256 digest before execution, replacing a direct network-to-shell pipeline.

.github/workflows/release.yml

http.rsMake HTTP limits unlimited by default +3/-3

Make HTTP limits unlimited by default

• Changes the default request rate and connection cap from fixed limits to effectively unlimited values while retaining environment-based configuration.

crates/ownpg-core/src/config/http.rs

@SHSharkar
SHSharkar merged commit 436125d into main Sep 13, 2026
10 of 22 checks passed
@SHSharkar
SHSharkar deleted the sazzad/harden-and-modernize-ownpg branch September 13, 2026 09:05
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Functions bypass safe privilege settings 🐞 Bug ⛨ Security
Description
returns_type accepts a crafted value such as `int LANGUAGE sql SECURITY DEFINER AS $$ SELECT 1 $$
--` because its probe remains one valid function statement, then returns the entire fragment
unchanged. When routine creation appends the configured clauses, the comment suppresses them,
allowing an injected owner-privileged function without the mandatory pinned search path.
Code

crates/ownpg-core/src/render.rs[R192-195]

+    let probe = format!(
+        "CREATE FUNCTION ownpg_probe_fn() RETURNS {trimmed} LANGUAGE sql AS $$ SELECT 1 $$"
+    );
+    let parsed = classify::classify(&probe).map_err(|error| Error::ArgumentInvalid {
Evidence
The validator interpolates the untrusted value before fixed function clauses but checks only that
parsing produced a function statement. The real renderer inserts that unchanged value before its
language, safe security-definer handling, pinned search path, and body, while final DDL verification
again checks only the overall statement kind.

crates/ownpg-core/src/render.rs[184-211]
crates/ownpg-core/src/tools/ddl/routine.rs[255-297]
crates/ownpg-core/src/tools/ddl/mod.rs[70-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`returns_type` validates only the probe's top-level statement kind, so an input can terminate the return type, inject function clauses and a body, and comment out the renderer-controlled clauses. This permits `SECURITY DEFINER` without the search-path protection normally added by the routine renderer.

## Fix Focus Areas
- crates/ownpg-core/src/render.rs[184-211]
- crates/ownpg-core/src/tools/ddl/routine.rs[255-297]

## Recommended Fix
Inspect the parsed function definition and require the untrusted text to populate only the return-type grammar node, rejecting comments or any supplied language, body, security, configuration, or other function clause. Add a regression test using `int LANGUAGE sql SECURITY DEFINER AS $$ SELECT 1 $$ --` and verify that normal scalar, `SETOF`, and `TABLE (...)` return types remain accepted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Rate-limit errors carry the wrong ID 🐞 Bug ≡ Correctness
Description
too_many_requests hard-codes JSON-RPC request ID 0 even though the guard runs before the request
body is parsed and cannot know the caller's ID. Any rate-limited request using another ID receives
an uncorrelatable response, while a notification incorrectly receives an ID-bearing response at all.
Code

crates/ownpg-core/src/server/http/mod.rs[R190-193]

        StatusCode::TOO_MANY_REQUESTS,
        axum::Json(serde_json::json!({
            "jsonrpc": "2.0",
-            "id": serde_json::Value::Null,
+            "id": 0,
Evidence
Both authentication throttling and ordinary request throttling call too_many_requests before
forwarding the request to the MCP service, but the helper always returns ID 0. The added test
checks only whether the fabricated ID has an allowed scalar type, not whether it matches the
incoming request.

crates/ownpg-core/src/server/http/mod.rs[187-205]
crates/ownpg-core/src/server/http/mod.rs[246-281]
crates/ownpg-core/src/server/http/mod.rs[313-322]
crates/ownpg-core/src/server/http/mod.rs[560-570]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HTTP guard emits a JSON-RPC error with a fixed ID before the MCP service parses the request body. The resulting ID does not match most requests and cannot correctly represent notifications.

## Fix Focus Areas
- crates/ownpg-core/src/server/http/mod.rs[187-205]
- crates/ownpg-core/src/server/http/mod.rs[246-322]
- crates/ownpg-core/src/server/http/mod.rs[560-570]

## Recommended Fix
Either apply request rate limiting after JSON-RPC decoding so the original request ID can be copied and notifications can remain response-free, or return a protocol-appropriate plain HTTP 429 without fabricating a JSON-RPC envelope. Extend tests with nonzero string and integer IDs and with a notification.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Shutdown can omit tool audit entries 🐞 Bug ☼ Reliability
Description
record_protocol_request and record_tool_call discard their spawn_blocking handles, while
Server::shutdown calls audit.flush() without retaining or awaiting the submitted audit writes.
When a request drains immediately before shutdown, a queued closure can acquire the audit lock after
the flush has synchronized existing records, leaving the final completed protocol or tool call
outside the shutdown durability boundary on process exit.
Code

crates/ownpg-core/src/server/mod.rs[R568-573]

+        let audit = Arc::clone(&self.audit);
+        tokio::task::spawn_blocking(move || {
+            if let Err(error) = audit.record(&entry) {
+                tracing::error!(%error, "the audit line could not be written");
+            }
+        });
Evidence
Both audit helpers return immediately after spawning blocking writes and do not retain their task
handles, while shutdown only invokes the sink's synchronous flush. That method locks the sink and
synchronizes records already written, but it has no knowledge of closures still queued by the
runtime, so a later task can acquire the mutex and write after flushing has completed.

crates/ownpg-core/src/server/mod.rs[263-306]
crates/ownpg-core/src/server/mod.rs[507-576]
crates/ownpg-core/src/server/mod.rs[588-611]
crates/ownpg-core/src/audit.rs[251-300]
crates/ownpg-core/src/server/mod.rs[298-303]
crates/ownpg-core/src/server/mod.rs[568-573]
crates/ownpg-core/src/server/mod.rs[588-607]
crates/ownpg-core/src/audit.rs[251-299]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Audit writes are submitted as detached blocking tasks whose handles are discarded, so `Server::shutdown()` can flush and synchronize the audit sink before already-accepted protocol or tool records have been written.

## Fix Focus Areas
- crates/ownpg-core/src/server/mod.rs[298-303]
- crates/ownpg-core/src/server/mod.rs[568-573]
- crates/ownpg-core/src/server/mod.rs[588-607]
- crates/ownpg-core/src/audit.rs[251-299]

## Recommended Fix
Track audit-write completion through a dedicated owned audit worker or by retaining all pending task handles, and route both protocol and tool records through the same tracked path. During shutdown, stop accepting new entries, drain or await every accepted write, and only then call `audit.flush()` to synchronize the sink. Preserve error logging for individual failed writes, and add a shutdown test proving that records submitted immediately before shutdown are present afterward.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Concurrent calls scramble audit chronology 🐞 Bug ◔ Observability
Description
Each completed request now submits an independent blocking job, and whichever job first acquires the
audit mutex determines its position and timestamp in the hash chain. Concurrent calls can therefore
appear in a different order from their completion and submission order, making incident timelines
misleading even though the resulting chain remains structurally valid.
Code

crates/ownpg-core/src/server/mod.rs[R569-573]

+        tokio::task::spawn_blocking(move || {
+            if let Err(error) = audit.record(&entry) {
+                tracing::error!(%error, "the audit line could not be written");
+            }
+        });
Evidence
Previously each record call entered the synchronous sink directly, while the changed code schedules
separate jobs before reaching it. The sink mutex serializes whichever job arrives first and
constructs each timestamp and previous-hash link at that point, without a sequence number or FIFO
mechanism.

crates/ownpg-core/src/server/mod.rs[298-303]
crates/ownpg-core/src/server/mod.rs[568-573]
crates/ownpg-core/src/audit.rs[251-285]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Independent blocking audit jobs race to acquire the sink mutex, so the append order no longer preserves the order in which completed calls submit their records. The hash chain remains valid but can describe a misleading event chronology.

## Fix Focus Areas
- crates/ownpg-core/src/server/mod.rs[298-303]
- crates/ownpg-core/src/server/mod.rs[568-573]
- crates/ownpg-core/src/audit.rs[251-285]

## Recommended Fix
Send completed audit entries through one FIFO worker that performs blocking writes serially, or assign submission sequence numbers and enforce that order before writing. Add a concurrent-call test that delays selected writes and verifies the resulting audit order remains the submission order.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Windows key permissions go unwarned 🐞 Bug ⛨ Security
Description
The expanded Windows warning tests profile.sslkey, but TLS resolution accepts OWNPG_SSLKEY and
PGSSLKEY into the separate resolved sslkey value. On Windows the permission check is
intentionally a no-op, so operators supplying either environment-sourced client key load it without
the new permissions_unchecked warning.
Code

crates/ownpg-core/src/config/resolve.rs[R557-563]

    if cfg!(windows)
        && (profile.password.is_some()
+            || profile.sslkey.is_some()
+            || profile
+                .ssh
+                .as_ref()
+                .is_some_and(|ssh| ssh.key_file.is_some())
Evidence
The PR adds a warning for profile TLS keys but leaves the same newly covered key type unwarned when
selected through either environment configuration layer. TLS consumes the resolved path, and
non-Unix permission checking always succeeds without inspecting mode bits.

crates/ownpg-core/src/config/resolve.rs[427-429]
crates/ownpg-core/src/config/resolve.rs[557-573]
crates/ownpg-core/src/config/libpq.rs[36-61]
crates/ownpg-core/src/config/profile.rs[259-283]
crates/ownpg-core/src/connect/tls.rs[158-178]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
The Windows permission warning only examines profile fields, so TLS keys supplied through `OWNPG_SSLKEY` or `PGSSLKEY` are not covered even though Windows cannot enforce their file modes.

Fix Focus Areas
- crates/ownpg-core/src/config/resolve.rs[557-573]
- crates/ownpg-core/src/config/resolve.rs[427-429]

Recommended Fix
Build the Windows warning condition from the resolved secret-file settings, including `sslkey`, rather than only profile fields. Apply the same approach to other environment or libpq sourced secret-file paths covered by the warning.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This security-sensitive PR spans SQL classification, secret-file permissions, HTTP protocol/rate limiting, audit concurrency, public API changes, and CI behavior across many independent logic sites, making multiple subtle defects plausible.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +192 to +195
let probe = format!(
"CREATE FUNCTION ownpg_probe_fn() RETURNS {trimmed} LANGUAGE sql AS $$ SELECT 1 $$"
);
let parsed = classify::classify(&probe).map_err(|error| Error::ArgumentInvalid {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Functions bypass safe privilege settings 🐞 Bug ⛨ Security

returns_type accepts a crafted value such as `int LANGUAGE sql SECURITY DEFINER AS $$ SELECT 1 $$
--` because its probe remains one valid function statement, then returns the entire fragment
unchanged. When routine creation appends the configured clauses, the comment suppresses them,
allowing an injected owner-privileged function without the mandatory pinned search path.
Agent Prompt
## Issue description
`returns_type` validates only the probe's top-level statement kind, so an input can terminate the return type, inject function clauses and a body, and comment out the renderer-controlled clauses. This permits `SECURITY DEFINER` without the search-path protection normally added by the routine renderer.

## Fix Focus Areas
- crates/ownpg-core/src/render.rs[184-211]
- crates/ownpg-core/src/tools/ddl/routine.rs[255-297]

## Recommended Fix
Inspect the parsed function definition and require the untrusted text to populate only the return-type grammar node, rejecting comments or any supplied language, body, security, configuration, or other function clause. Add a regression test using `int LANGUAGE sql SECURITY DEFINER AS $$ SELECT 1 $$ --` and verify that normal scalar, `SETOF`, and `TABLE (...)` return types remain accepted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 190 to +193
StatusCode::TOO_MANY_REQUESTS,
axum::Json(serde_json::json!({
"jsonrpc": "2.0",
"id": serde_json::Value::Null,
"id": 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Rate-limit errors carry the wrong id 🐞 Bug ≡ Correctness

too_many_requests hard-codes JSON-RPC request ID 0 even though the guard runs before the request
body is parsed and cannot know the caller's ID. Any rate-limited request using another ID receives
an uncorrelatable response, while a notification incorrectly receives an ID-bearing response at all.
Agent Prompt
## Issue description
The HTTP guard emits a JSON-RPC error with a fixed ID before the MCP service parses the request body. The resulting ID does not match most requests and cannot correctly represent notifications.

## Fix Focus Areas
- crates/ownpg-core/src/server/http/mod.rs[187-205]
- crates/ownpg-core/src/server/http/mod.rs[246-322]
- crates/ownpg-core/src/server/http/mod.rs[560-570]

## Recommended Fix
Either apply request rate limiting after JSON-RPC decoding so the original request ID can be copied and notifications can remain response-free, or return a protocol-appropriate plain HTTP 429 without fabricating a JSON-RPC envelope. Extend tests with nonzero string and integer IDs and with a notification.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +569 to +573
tokio::task::spawn_blocking(move || {
if let Err(error) = audit.record(&entry) {
tracing::error!(%error, "the audit line could not be written");
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Concurrent calls scramble audit chronology 🐞 Bug ◔ Observability

Each completed request now submits an independent blocking job, and whichever job first acquires the
audit mutex determines its position and timestamp in the hash chain. Concurrent calls can therefore
appear in a different order from their completion and submission order, making incident timelines
misleading even though the resulting chain remains structurally valid.
Agent Prompt
## Issue description
Independent blocking audit jobs race to acquire the sink mutex, so the append order no longer preserves the order in which completed calls submit their records. The hash chain remains valid but can describe a misleading event chronology.

## Fix Focus Areas
- crates/ownpg-core/src/server/mod.rs[298-303]
- crates/ownpg-core/src/server/mod.rs[568-573]
- crates/ownpg-core/src/audit.rs[251-285]

## Recommended Fix
Send completed audit entries through one FIFO worker that performs blocking writes serially, or assign submission sequence numbers and enforce that order before writing. Add a concurrent-call test that delays selected writes and verifies the resulting audit order remains the submission order.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +568 to +573
let audit = Arc::clone(&self.audit);
tokio::task::spawn_blocking(move || {
if let Err(error) = audit.record(&entry) {
tracing::error!(%error, "the audit line could not be written");
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Shutdown can omit tool audit entries 🐞 Bug ☼ Reliability

record_protocol_request and record_tool_call discard their spawn_blocking handles, while
Server::shutdown calls audit.flush() without retaining or awaiting the submitted audit writes.
When a request drains immediately before shutdown, a queued closure can acquire the audit lock after
the flush has synchronized existing records, leaving the final completed protocol or tool call
outside the shutdown durability boundary on process exit.
Agent Prompt
## Issue description
Audit writes are submitted as detached blocking tasks whose handles are discarded, so `Server::shutdown()` can flush and synchronize the audit sink before already-accepted protocol or tool records have been written.

## Fix Focus Areas
- crates/ownpg-core/src/server/mod.rs[298-303]
- crates/ownpg-core/src/server/mod.rs[568-573]
- crates/ownpg-core/src/server/mod.rs[588-607]
- crates/ownpg-core/src/audit.rs[251-299]

## Recommended Fix
Track audit-write completion through a dedicated owned audit worker or by retaining all pending task handles, and route both protocol and tool records through the same tracked path. During shutdown, stop accepting new entries, drain or await every accepted write, and only then call `audit.flush()` to synchronize the sink. Preserve error logging for individual failed writes, and add a shutdown test proving that records submitted immediately before shutdown are present afterward.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 557 to +563
if cfg!(windows)
&& (profile.password.is_some()
|| profile.sslkey.is_some()
|| profile
.ssh
.as_ref()
.is_some_and(|ssh| ssh.key_file.is_some())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Windows key permissions go unwarned 🐞 Bug ⛨ Security

The expanded Windows warning tests profile.sslkey, but TLS resolution accepts OWNPG_SSLKEY and
PGSSLKEY into the separate resolved sslkey value. On Windows the permission check is
intentionally a no-op, so operators supplying either environment-sourced client key load it without
the new permissions_unchecked warning.
Agent Prompt
Issue description
The Windows permission warning only examines profile fields, so TLS keys supplied through `OWNPG_SSLKEY` or `PGSSLKEY` are not covered even though Windows cannot enforce their file modes.

Fix Focus Areas
- crates/ownpg-core/src/config/resolve.rs[557-573]
- crates/ownpg-core/src/config/resolve.rs[427-429]

Recommended Fix
Build the Windows warning condition from the resolved secret-file settings, including `sslkey`, rather than only profile fields. Apply the same approach to other environment or libpq sourced secret-file paths covered by the warning.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant