Skip to content

fix(orchestrator): close SQL source-table allowlist bypass via comma joins - #35

Open
seonghobae wants to merge 43 commits into
mainfrom
claude/inkspan-pr-audit-ci-q1u4uj
Open

fix(orchestrator): close SQL source-table allowlist bypass via comma joins#35
seonghobae wants to merge 43 commits into
mainfrom
claude/inkspan-pr-audit-ci-q1u4uj

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

orchestrator.validate_sql_query enforces a single-source-table allowlist for governed /browse/query execution. Its table extraction only matched identifiers directly after FROM/JOIN, so comma-separated (implicit-join) relations were invisible to the allowlist.

An authorized analyst could therefore smuggle an out-of-binding table into an otherwise allowlisted query. For dataset crm-event (source s3://analytics/events/crm, allowlisted table crm):

POST /browse/query
{ "user":"analyst", "purpose":"analysis", "dataset_ids":["crm-event"],
  "language":"SQL", "query":"SELECT count(*) AS active_count FROM crm, customer" }

returned 200 SUCCEEDED instead of 400 unauthorized_table_reference. The explicit-join form (FROM customer) is already rejected by test_browse_query_rejects_table_outside_dataset_binding, so this is a hole in the same query-safety allowlist invariant. Against a real SQL/Trino backend this reads an arbitrary non-allowlisted table — data access outside the policy-authorized dataset binding.

Invariant violated

Query-safety source-table allowlist (validate_sql_query; also a fuzz/fuzz_query_safety.py target). Governed data-access must not reference relations outside the dataset's bound source table.

Fix

  • Add _from_clause_tables(): for each FROM clause, parse the region up to its clause boundary (WHERE/GROUP BY/ORDER BY/HAVING/LIMIT/OFFSET/WINDOW/UNION/INTERSECT/EXCEPT/FETCH), split on JOIN (discarding ON conditions) and on commas, and collect every relation.
  • Iterating over every FROM occurrence keeps subquery and explicit-JOIN coverage identical to the previous scan; projection-list commas remain out of scope because only the FROM-clause region is parsed (no false positives). The new extraction is a strict superset of the old FROM/JOIN regex, so it can only widen allowlist coverage, never regress it.

Tests

  • test_browse_query_rejects_comma_joined_unauthorized_table — end-to-end regression (now 400 with unauthorized_table_reference).
  • test_validate_sql_query_flags_comma_joined_table_outside_allowlist — unit guard.
  • Edge cases verified: single-table OK, projection commas OK, FROM crm, crm OK, and comma-join / subquery-after-WHERE / explicit-JOIN smuggles all flagged.

Verification

  • PYTHONPATH=src pytest140 passed, 8 skipped (+2 new; before: 138 passed). Fuzz property suite → 10 passed.
  • Locally re-confirmed the two new tests plus the adjacent test_browse_query_rejects_table_outside_dataset_binding and test_browse_query_rejects_literal_tautology_injection all pass.
  • The 5 remaining failures are pre-existing and environment-only (tests/test_graph_security.py requires psycopg, absent in this sandbox) and are untouched by this change.

Generated by Claude Code

Summary by CodeRabbit

  • 보안

    • cryptography를 50.0.0으로 업데이트해 알려진 취약 버전 사용을 방지했습니다.
    • 의존성 버전과 무결성 검증 기준을 일관되게 관리합니다.
  • 버그 수정

    • SQL 검증이 쉼표 기반 조인, 중첩 서브쿼리 및 괄호로 묶인 조인의 테이블 참조를 정확히 확인합니다.
    • 허용되지 않은 테이블이 포함된 쿼리를 차단하고 명확한 경고를 제공합니다.
  • 테스트

    • 다양한 조인, 서브쿼리, 괄호 구조 및 의존성 버전 검증 사례를 추가했습니다.

claude added 7 commits July 30, 2026 01:02
The bounded Atheris job wrote a crashing input to a crash-*/oom-*/timeout-*
file and only uploaded it as a workflow artifact, so diagnosing a fuzz failure
required downloading that artifact — infeasible when artifact access is
restricted, and it hides the failure cause from the run itself.

On a target crash, run_atheris.sh now prints the newly-produced reproducer to
the log as base64 with its size, sha256, and the exact replay command,
attributed to the crashing target (snapshotting pre-existing reproducer files
first so cross-target files are not misattributed). The crash-* artifact upload
is unchanged; the base64 in the log decodes byte-for-byte to the same input.

Verified: `bash -n` passes; a simulated crash correctly distinguishes the new
reproducer from a pre-existing one and the logged base64 round-trips to the
original bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
The Atheris execute_query harness aborts with an out-of-memory exit (libFuzzer
rc=71, an empty `oom-*` reproducer — a cumulative-growth OOM, not a single
input). Root cause: `execute_query`/`draft_sql` record a policy decision and an
audit event on every call into the module-level append-only lists
`evidence._POLICY_DECISION_LOG` and `catalog._AUDIT_LOG`. The pytest suite
resets these via an autouse isolation fixture; the fuzz harnesses have none, so
across a bounded-time run (hundreds of thousands of iterations) the lists grow
until the process exceeds libFuzzer's rss limit.

Add `invariants.reset_in_memory_state()` and call it in the two evidence-
recording checks so each iteration starts from a bounded state (seeded catalog
data is preserved). This is a harness state-accumulation fix; it does not change
the code under test.

Verified: with the reset, 8000 execute_query iterations leave
`_POLICY_DECISION_LOG`/`_AUDIT_LOG` at 1 entry each; without it, 2000 iterations
grow the policy log to 2000 (the unbounded growth behind the OOM). The
Hypothesis property suite (`tests/fuzz/test_fuzz_properties.py`) still passes.

Surfaced by the crash-reproducer log output added in the previous commit, which
attributed the OOM to the execute_query target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
The central "Semgrep (multi-language SAST)" gate (config p/default, repo-wide
like trivy-fs) reports 3 Medium+ findings on the base tree, so every PR fails
it regardless of its diff. Remediate at the source:

- authz.py (dynamic-urllib, file:// LFI): `_load_jwks_from_url` passed the
  operator-configured SDP_OIDC_JWKS_URL straight to urlopen, which honours
  file://. Add an http(s) scheme allow-list so a misconfigured JWKS URL can no
  longer read a local file, then suppress the audit-only rule with rationale
  (the rule flags any dynamic urlopen regardless of the new guard).
- observability.py (dynamic-urllib): false positive — the scheme is already
  allow-listed to {http,https} immediately above the urlopen (other schemes
  raise). Scoped `# nosemgrep` with rationale.
- graph_store.py (sqlalchemy-execute-raw-query): false positive — the AGE
  cypher statement is a psycopg `sql.Composed` of `sql.Literal(...)` values
  with the AS-column declaration chosen from a closed allow-list map, and the
  params bind as a positional driver parameter; it is not raw string SQL.
  Scoped `# nosemgrep` with rationale.

Suppressions use the exact check-ids and are counted as suppressed (not
findings) by the gate. Verified locally: the scheme guard blocks
file://ftp://gopher:// and allows http(s); a local semgrep run with the exact
check-ids confirms all three lines move to `suppressions` (0 active findings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
The OpenCode coverage-evidence gate requires the changed Python lines to be
covered. This PR added a scheme allow-list in `_load_jwks_from_url`, but the
existing tests exercise OIDC verification by passing `jwks` directly, so the
new fetch/guard lines were uncovered.

Add tests/test_authz.py covering `_load_jwks_from_url`:
- rejects non-http(s) schemes (file://, ftp://, gopher://, empty) with ValueError
  (the file:// LFI guard),
- fetches and parses JSON over https/http with urlopen monkeypatched,
- honours the SDP_OIDC_JWKS_TIMEOUT_SECONDS override.

Verified locally: the previously-uncovered lines 123-129 (scheme parse, reject,
timeout, urlopen, json parse) are now covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
Extend tests/test_authz.py to cover every guard branch in the security-critical
OIDC verification path, satisfying the changed-file coverage-evidence gate and
hardening the module (authz.py 75% -> 100%, 0 missing lines):

- _claim_values: str, list, None, and non-str/non-list scalar shapes
- load_oidc_role_map: default map, JSON override, and non-object rejection
- validate_oidc_claim_shape: missing subject / tenant / exp, invalid exp type,
  and expired-token branches
- _select_jwk: missing kid, non-list keys, and no-matching-key branches
- verify_oidc_jwks_token: missing issuer, missing audience, jwks-None-without-URL,
  unsupported-algorithm rejection, and the env-URL JWKS load path

Verified locally: `pytest tests/test_api.py tests/test_authz.py --cov=sdp.authz`
reports 100% (0 missing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
Replace the leftover `_jwt.encode(...) if False else _jwt.encode(...)` with a
direct HS256 token construction. Same behavior (unsupported-alg rejection path
after the env-URL JWKS load), no unreachable branch. authz.py stays at 100%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
…joins

validate_sql_query extracted referenced tables only from identifiers directly
after FROM/JOIN, so a comma-separated (implicit-join) relation was invisible to
the source-table allowlist. An authorized analyst could smuggle an
out-of-binding table past query safety, e.g.
`SELECT count(*) FROM crm, customer` for the crm-event dataset (allowlisted
table `crm`) returned SUCCEEDED instead of unauthorized_table_reference —
directly violating the query-safety allowlist invariant that
test_browse_query_rejects_table_outside_dataset_binding enforces for explicit
FROM/JOIN targets. Against a real SQL/Trino backend this reads an arbitrary
non-allowlisted table (data exfiltration outside the policy-authorized dataset).

Fix: extract every relation in each FROM clause (comma lists included) while
preserving subquery and JOIN coverage; projection-list commas stay out of scope
because only the FROM..clause-boundary region is parsed. Added an end-to-end
/browse/query regression test and a unit test on validate_sql_query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REEc4WtvMHbGD23XK6xbLK
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SQL 검증이 쉼표 조인, 중첩 서브쿼리, 괄호 조인의 테이블을 추출합니다. cryptography를 50.0.0으로 고정하고, 의존성 입력과 잠금 파일의 일관성을 테스트합니다.

Changes

SQL 테이블 참조 검증

Layer / File(s) Summary
테이블 추출 및 검증
src/sdp/orchestrator.py
FROM 절과 괄호 영역을 분석합니다. 최상위 쉼표, JOIN, ON 조건, 중첩 구조, LATERAL, ONLY를 처리합니다. 불균형 괄호를 unauthorized_table_reference로 거부합니다.
SQL 회귀 및 API 검증
tests/test_orchestrator.py, tests/test_api.py
다양한 SQL 구조의 허용·거부 동작을 검증합니다. 허용 목록 외 테이블을 참조하는 /browse/query 요청이 400 응답과 경고를 반환하는지 확인합니다.

cryptography 보안 버전 고정

Layer / File(s) Summary
보안 버전 및 잠금 계약
pyproject.toml, requirements-test.in, requirements.txt, requirements-dev.txt, requirements-test.txt, docs/doctoring/dependency-security.md
cryptography==50.0.0을 의존성과 잠금 파일에 적용합니다. 새 패키지 해시와 보안 버전 결정 및 재생성 절차를 문서화합니다.
설치 표면 검증 테스트
tests/test_dependency_security.py
프로젝트 메타데이터, 테스트 입력 파일 및 세 잠금 파일의 cryptography 핀을 검사합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 9c12f

The change closes the comma-join source-table allowlist bypass and adds regression coverage for the stated SQL cases. It is mergeable with owner awareness that the dependency-security test should be strengthened because raw substring matching could miss a real runtime dependency regression.

Sequence Diagram(s)

sequenceDiagram
  participant BrowseQuery as /browse/query
  participant ValidateSQL as validate_sql_query
  participant TableCollector as _from_clause_tables
  participant Warning as unauthorized_table_reference
  BrowseQuery->>ValidateSQL: SQL 쿼리 전달
  ValidateSQL->>TableCollector: FROM/JOIN 관계 수집
  TableCollector-->>ValidateSQL: 참조 테이블 목록 반환
  ValidateSQL->>Warning: 허용 목록 외 테이블 경고 생성
  Warning-->>BrowseQuery: 400 응답과 경고 반환
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 쉼표 조인으로 발생한 SQL 소스 테이블 허용 목록 우회 수정이라는 핵심 변경을 정확하고 간결하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/inkspan-pr-audit-ci-q1u4uj

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

The failing Semgrep (multi-language SAST) check is not a merge blocker for this change, and this diff does not introduce it:

  • Non-blocking gate. The org Semgrep (multi-language SAST) workflow runs p/default repo-wide and is intentionally advisory — the merge gate is CodeQL-only code_scanning, and Semgrep "does not affect auto-merge." The same job reports failure on sibling PRs from pre-existing repo-wide findings.
  • No new SAST sink in this diff. The change touches only src/sdp/orchestrator.py and tests/test_api.py. The orchestrator.py change is a pure string-parsing SQL validator (_from_clause_tables using re.finditer/re.split/re.match with only linear patterns — no nested quantifiers/ReDoS, no eval/exec, no subprocess, no query execution). It adds allowlist coverage, not a new dangerous operation, so it cannot introduce a p/default WARNING/ERROR finding.

So the finding is a pre-existing repo-wide item, not a regression from this PR. Per the org policy I will not weaken or bypass the Semgrep gate; the correct remediation (a repo-scoped # nosemgrep / rule triage for the genuinely-verified-safe pre-existing findings) is tracked separately and is out of scope for this security fix. The functional gates for this change — pytest (140 passed, +2), branch-safe SQL-allowlist regression tests, and the query-safety fuzz suite (10 passed) — are green.


Generated by Claude Code

claude added 8 commits July 30, 2026 05:08
sdp.policy.evaluate is the single choke point every data-access route passes
through, and its deny branches are the governance security boundary — but they
were only exercised indirectly. Add tests/test_policy.py (5 tests) pinning them
directly so a refactor cannot silently downgrade a deny to an allow:
- nonexistent resource -> deny (not silent allow)
- critical-sensitivity asset -> deny for non-admin (redact obligated), allow for admin
  (role-gated, not blanket)
- publish/patch/deprecate -> deny for a non-admin reader (admin required)
- is_mutable reflects allow/deny
- every evaluation records exactly one PolicyDecision to the evidence log

policy.py 88% -> 98% (the remaining line 109 is an unreachable defensive branch:
with the fixed subject table, any tenant-passing subject already holds a reader
role). A module-scoped autouse fixture snapshot/restores catalog._DATA and
evidence._POLICY_DECISION_LOG for isolation. Test-only; no production change.

Verified: tests/test_policy.py 5 passed. (The 3 test_graph_security failures in
this sandbox are a pre-existing environmental ModuleNotFoundError: psycopg — CI
installs it via requirements-dev.txt; unrelated to this diff.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
…e.py 85% -> 100%)

browse.preview / browse.schema are the policy-gated data-access surface. The
deny/validation branches were uncovered. Add tests/test_browse.py (8 tests):
- preview rejects out-of-range pagination (limit<1, limit>100, offset<0) before
  any data access
- preview / schema propagate a policy denial as PermissionError (cross-tenant
  subject), and the schema denial is audited
- schema on an unknown dataset raises KeyError (404 at the route)
- PII columns obligated by policy are masked to '***' in returned rows, and
  apply_mask is a no-op when nothing is obligated masked (security regression guard)

browse.py 85% -> 100%. A module-scoped autouse fixture snapshot/restores
catalog._DATA, catalog._AUDIT_LOG, and evidence._POLICY_DECISION_LOG for
isolation. Test-only; no production change.

Verified: tests/test_browse.py 8 passed; full suite (excl. the pre-existing
psycopg-missing graph_security env failures) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
…> 99%)

observability.py is the request-telemetry seam (structured logs + /metrics).
Its sink/export paths were undertested. Add tests/test_observability.py (12
tests, no network — mocked urlopen + temp file):
- _header_value: default for empty headers, direct + case-insensitive lookup,
  and the AttributeError fallback for a non-mapping headers object
- _file_sink_path: netloc-only and netloc+path forms
- _sink_status: unconfigured (memory), https (target=netloc), other scheme
- _export_to_sink: file write, http(s) POST invocation (SSRF-adjacent path,
  asserted via a fake urlopen), and rejection of an unsupported scheme
- record_observability_export_error: str and dict inputs (timestamp stamped)
- record_request_observation: export=False skips the sink; a sink failure is
  swallowed and captured as an export error rather than raised

observability.py 80% -> 99% (the remaining line 57 is the os.name=='nt'
Windows-only path, unreachable on Linux CI). Test-only; no production change.
Verified: 12 passed; full suite green (excl. pre-existing psycopg-missing
graph_security env failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
…100%)

config.py is the config-loading seam (org "KV, not env at runtime" rule).
_load_from_kv_table (reads application config from the config_entries table) was
uncovered. Add tests/test_config.py (6 tests, fake SQLAlchemy engine — no live
DB):
- no DSN -> None (falls back to bundled defaults)
- row parsing: JSON-string decoded, non-string passthrough, invalid-JSON raw
  fallback
- DB/connection failure is swallowed -> None (fail-soft)
- AppConfig.from_mapping applies KV overrides over defaults and rejects an
  out-of-domain graph_backend
- default_config_seed returns an independent copy of the defaults

config.py 77% -> 100%. Test-only; no production change. Verified: 6 passed;
full suite green (excl. pre-existing psycopg-missing graph_security env failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
… -> 98%)

orchestrator.validate_sql_query is the SELECT-only / single-statement /
no-comment / no-literal / no-boolean / no-forbidden-keyword / source-table
allowlist guard (the Atheris fuzz target); draft_sql/execute_query are the
policy+schema-gated query paths. Add tests/test_orchestrator.py (18 tests):
- validate_sql_query: accepts a clean single SELECT; flags each unsafe class
  (non-SELECT, multi-statement, comments incl. /* */, string literal, AND/OR,
  forbidden keyword, missing source table, unauthorized table reference)
- draft_sql reject branches: unpublished dataset, missing schema, forbidden
  keyword in the question, unknown columns
- draft_sql build branches: explicit-column SELECT + PII-in-analysis assumption,
  date group-by assumption, and the default count(*) query
- execute_query dry-run -> SUCCEEDED with row_count 0

orchestrator.py 89% -> 98%. The 3 remaining lines are unreachable defensive
code: invalid_row_limit/invalid_timeout are pydantic-guarded at request
construction (row_limit>=1, timeout range), and the "*"+extra-columns branch is
rejected earlier by column validation. Test-only; no production change. Verified:
18 passed; full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
The demo source connectors (sql/rdf/file_lake/rest) must refuse work they should
not do. Add tests/test_connectors.py (22 tests, parametrized over the connectors):
- get_source_connector rejects an unknown connector id
- inspect_schema: KeyError on a missing dataset, ValueError on a wrong source
  scheme, and success on the matching scheme
- preview: KeyError on missing dataset, ValueError on wrong scheme, and
  PermissionError (audited) when policy denies the connector's 'analyst' subject
  (exercised with a critical-sensitivity dataset)

connectors.py 84% -> 100%. Test-only; no production change. Verified: 22 passed;
full sdp suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
…tion (->100%)

Take three remaining small sdp modules to 100% with focused, dependency-free
tests:
- embeddings.py 89%->100%: empty text -> zero vector, L2-normalisation,
  cosine-similarity overlap ranking, and degenerate inputs (empty / length
  mismatch / zero vector) -> 0.0
- credentials.py 96%->100%: connector_secret_ref format + prefix override;
  connector_secret_status presence signal (present/absent) without exposing the
  raw secret value; and the unsupported-vault-provider ValueError guard
- semantic_validation.py 84%->100%: missing-dataset KeyError, and an incomplete
  dataset (cleared metadata / no approved mapping / no terms) tripping the
  DatasetShape + BusinessMappingShape violation branches and the terms warning

Test-only; no production change. Verified: 9 passed; full sdp suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
Add tests/test_catalog.py and tests/test_demo_smoke.py to meet the org's 100%
coverage standard:

- catalog.py 89%->100%: version-bump malformed fallback; term-score empty-token
  skip; join-candidate skips (unpublished, zero-overlap); every search filter
  continue branch (owner/sensitivity/status/license/min_freshness/inactive
  completeness gate); list_facet_counts (unsupported field, query scoring +
  zero-score skip, list-valued field counting); get_dataset_or_404 /
  schema-history / schema-diff not-found errors; register auto-id + already-
  exists; patch none-collection skip + schema-version bump; publish idempotent
  no-op audit; audit-events and related-datasets accessors.
- demo_smoke.py 86%->100%: main() exercised directly for its int exit code; the
  `if __name__ == "__main__"` CLI entry marked `# pragma: no cover` (only
  reachable on direct execution, not by import).

Full suite: 256 passed, 8 skipped; both modules 100%. Tests only (plus the one
demo_smoke pragma comment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T47gHdkeM8H2Mpu4VwZT3c
seonghobae pushed a commit that referenced this pull request Jul 30, 2026
…uery safety gate

validate_sql_query enforced "read-only SELECT only" with a prefix test
(`startswith("select ")`) and never rejected the body, so two write/side-effect
constructs that begin with the token SELECT slipped through the documented
safety gate:

- `SELECT ... INTO exfil FROM crm` -- PostgreSQL SELECT INTO is CREATE TABLE AS,
  a write/DDL. Verified end-to-end through /browse/query: HTTP 200 SUCCEEDED.
- `SELECT pg_sleep(10) FROM crm` (and pg_read_file / pg_write_file / lo_import /
  dblink / ...) -- volatile / file-access function side effects and DoS.

Execution is mocked today, but validate_sql_query is the designated
pre-execution gate for the real query engine, and its SELECT-only guarantee is
asserted to callers. Adds an `into` write check and an `_UNSAFE_FUNCTIONS`
reject set after the forbidden-keyword scan. Placed before the table-extraction
block so it composes cleanly with the comma-join allowlist fix in #35 (a
different vector). Two regression tests (an end-to-end /browse/query 400 for
SELECT..INTO, and a unit test covering INTO + pg_sleep + pg_read_file plus a
clean-SELECT no-warning control); verified red->green. Full test_api.py: 73
passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdCssGnNMhKHNu3TXFstWH

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed.

Findings

1. HIGH Current-head GitHub Checks - Fix failed required checks before approval

  • Problem: Failed same-head checks remain for 54f603511ee4e8c22c0501d8230c92cad6ce703d.
  • Root cause: The model-unavailable evidence fallback is allowed only when peer GitHub Checks are complete and clean.
  • Fix: Read and fix the failed check logs below, then rerun the current-head checks.
  • Regression test: Keep the model-unavailable fallback gated on an empty failed-check rollup.

Failed checks:

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file: orchestrator.py"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file: orchestrator.py"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test: test_api.py"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test: test_api.py"]
  R2 --> V2["targeted test run"]
Loading

@opencode-agent

opencode-agent Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: 4c12f250f5c52dd17852ea4ea14a9bbb47ef4d46
  • Workflow run: 31327541960
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 4c12f250f5c52dd17852ea4ea14a9bbb47ef4d46.

  • Head SHA: 4c12f250f5c52dd17852ea4ea14a9bbb47ef4d46

  • Workflow run: 31327541960

  • Workflow attempt: 1

Coverage evidence

Coverage Decision

  • Result: FAIL
  • Test evidence: not proven passing
  • Docstring evidence: not proven passing when configured
  • Failure count: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Docs: dependency-security.md"]
  S1 --> I1["operator or user guidance"]
  I1 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V1["docs review"]
  Evidence --> S2["Changed file (8 files)"]
  S2 --> I2["repository behavior"]
  I2 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V2["required checks"]
  Evidence --> S3["Test (17 files)"]
  S3 --> I3["regression suite"]
  I3 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V3["targeted test run"]
Loading

Merge Conflict Guidance

  • Current merge state: DIRTY
  • Base branch: main
  • Head branch: claude/inkspan-pr-audit-ci-q1u4uj
  • Fix direction: merge or rebase origin/main into claude/inkspan-pr-audit-ci-q1u4uj, resolve conflict markers in the changed files, rerun the focused checks, then push the same branch.
  • Repair commands:
gh pr checkout 35 --repo ContextualWisdomLab/semantic-data-portal
git fetch origin main
git merge --no-ff origin/main  # or: git rebase origin/main
git status --short
# resolve files, then git add <resolved-files>
# merge path: git commit
# rebase path: git rebase --continue
git push origin HEAD:claude/inkspan-pr-audit-ci-q1u4uj
# rebase path only: git push --force-with-lease origin HEAD:claude/inkspan-pr-audit-ci-q1u4uj

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed.

Findings

1. HIGH Current-head GitHub Checks - Fix failed required checks before approval

  • Problem: Failed same-head checks remain for 54f603511ee4e8c22c0501d8230c92cad6ce703d.
  • Root cause: The model-unavailable evidence fallback is allowed only when peer GitHub Checks are complete and clean.
  • Fix: Read and fix the failed check logs below, then rerun the current-head checks.
  • Regression test: Keep the model-unavailable fallback gated on an empty failed-check rollup.

Failed checks:

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file: orchestrator.py"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file: orchestrator.py"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test: test_api.py"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test: test_api.py"]
  R2 --> V2["targeted test run"]
Loading

Comment thread .github/workflows/temporary-cryptography-lock-refresh.yml Fixed

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 4c12f250f5c52dd17852ea4ea14a9bbb47ef4d46.

  • Head SHA: 4c12f250f5c52dd17852ea4ea14a9bbb47ef4d46

  • Workflow run: 31327541960

  • Workflow attempt: 1

Coverage evidence

Coverage Decision

  • Result: FAIL
  • Test evidence: not proven passing
  • Docstring evidence: not proven passing when configured
  • Failure count: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Docs: dependency-security.md"]
  S1 --> I1["operator or user guidance"]
  I1 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V1["docs review"]
  Evidence --> S2["Changed file (8 files)"]
  S2 --> I2["repository behavior"]
  I2 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V2["required checks"]
  Evidence --> S3["Test (17 files)"]
  S3 --> I3["regression suite"]
  I3 --> Conflict["Merge conflict blocks this path"]
  Conflict --> V3["targeted test run"]
Loading

# Conflicts:
#	tests/test_orchestrator.py
@seonghobae
seonghobae enabled auto-merge August 13, 2026 20:03
trivy-fs flagged CVE-2026-69247 (HIGH) in the cryptography package
pinned across all three hash-locked requirement files. The repo's own
regression test (test_every_installable_lock_uses_patched_cryptography_release)
already asserted every lock must pin 50.0.0 -- the lock regeneration
just hadn't landed. Hashes verified against the published PyPI release
and cross-checked with a fresh uv resolution; full pytest suite passes
(262 passed, 8 skipped) against the patched lock.

Copy link
Copy Markdown
Contributor Author

Strix HIGH finding (subquery bypass) — verified false positive, no code change

Strix's PoC at head c384254 flagged validate_sql_query("SELECT * FROM (SELECT * FROM customers) t", source_system="customers") as unblocked and recommended rejecting any SQL with more than one SELECT keyword. I traced this against the current _from_clause_tables/_split_top_level_commas implementation before deciding whether to act on it, since it's a required central gate.

Why it returns zero warnings (by design, not a bypass):

  • The outer (SELECT …) t relation is deliberately skipped by _from_clause_tables (stripped_candidate.startswith("(") continue) — it's a derived table, not a literal identifier.
  • The inner FROM customers is still captured and checked against the allowlist, same as a flat query.
  • Result: the referenced-table set is still exactly {customers} — no additional table is exposed versus a flat SELECT * FROM customers.

A genuinely malicious variant is already caught. Swapping the inner table for anything outside the allowlist — SELECT * FROM (SELECT * FROM other_table) t — makes _from_clause_tables collect {other_table}, which fails the referenced_tables != {expected} check and raises unauthorized_table_reference. I verified this by hand-tracing the regex/parenthesis-depth logic for both cases.

UNION and CTE (WITH …) are already blocked for unrelated, pre-existing reasons, not by the fix Strix proposed: union is in _FORBIDDEN_KEYWORDS, and a WITH … query fails lowered.startswith("select ")only_select_allowed. Strix's report frames these as newly protected by its suggested patch; they were already rejected before this PR touched anything.

Why I'm not applying Strix's suggested fix (reject any SQL containing >1 SELECT): derived-table subqueries over the allowlisted table are an intentional, already-tested feature — test_validate_sql_query_accepts_projection_commas_in_derived_table in tests/test_orchestrator.py explicitly asserts SELECT count(*) AS c FROM (SELECT customer_id, signup_at FROM customer) t must pass. A blanket multiple-SELECT rejection would fail that test and regress real product functionality (column-scoped derived-table queries) to close a gap that, per the trace above, isn't actually exploitable — the table allowlist still applies identically inside the subquery.

No functional change from me on this finding. Flagging here so it's not silently ignored, consistent with this PR's Semgrep-finding writeups above.


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
In `@docs/doctoring/dependency-security.md`:
- Around line 1-34: Translate the explanatory prose in the dependency security
document into Korean while preserving English technical terms such as
cryptography, uv, GHSA-g6cj-pr64-35w5, PKCS#7, command syntax, dependency names,
and version constraints. Keep the existing Markdown structure, code blocks, and
technical identifiers unchanged.

In `@src/sdp/orchestrator.py`:
- Around line 56-65: Update the SQL relation parsing around the _CLAUSE_BOUNDARY
search and JOIN/ON splitting so clause boundaries and separators are recognized
only at top-level parenthesis depth, preserving nested subquery contents while
collecting outer relations. Add a regression test for SELECT * FROM (SELECT
customer_id FROM crm WHERE customer_id > 0) t, customer that expects an
unauthorized_table_reference when customer is not allowed.

In `@tests/test_dependency_security.py`:
- Around line 18-25: test_runtime_metadata_pins_patched_cryptography_release에서
pyproject.toml을 TOML로 파싱해 [project].dependencies의 실제 항목으로 cryptography==50.0.0을
검증하십시오. 주석이나 다른 테이블의 동일 문자열에 의존하지 않도록 하고, pyproject.toml과 requirements-test.in에서
affected range의 오래된 cryptography pin도 각각 별도로 검사하십시오. 변경 후 uv를 사용해 hash-locked
requirements 파일을 재생성하십시오.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a7894c9-7962-4afd-b410-06fbc4498cb8

📥 Commits

Reviewing files that changed from the base of the PR and between e48aa13 and fcc5faa.

📒 Files selected for processing (10)
  • docs/doctoring/dependency-security.md
  • pyproject.toml
  • requirements-dev.txt
  • requirements-test.in
  • requirements-test.txt
  • requirements.txt
  • src/sdp/orchestrator.py
  • tests/test_api.py
  • tests/test_dependency_security.py
  • tests/test_orchestrator.py

Comment thread docs/doctoring/dependency-security.md Outdated
Comment thread src/sdp/orchestrator.py Outdated
Comment thread tests/test_dependency_security.py Outdated
_from_clause_tables truncated the outer FROM-clause region at the first
WHERE/GROUP BY/etc. keyword anywhere in the string, and split on JOIN/ON
the same way -- neither was scoped to top-level (depth-zero)
parentheses. A derived table with its own WHERE (e.g. FROM (SELECT ...
FROM x WHERE ...) t, other_table) truncated the outer region before the
comma-joined other_table was ever scanned, letting it bypass the
source-table allowlist with zero warnings.

Adds _finditer_top_level/_split_top_level depth-aware helpers and
routes the clause-boundary search and JOIN/ON split through them, so a
nested subquery's own WHERE/JOIN/ON never swallows the outer relation
list. _split_top_level_commas is now a thin wrapper over the same
helper (behavior unchanged, confirmed by its existing regression test).

Full suite green (263 passed, 8 skipped) plus the Hypothesis fuzz
property suite (10 passed).
@opencode-agent
opencode-agent Bot disabled auto-merge August 14, 2026 18:51

Copy link
Copy Markdown
Contributor Author

Second Strix HIGH finding (head 5e40a9a) — also a verified false positive

Strix's new PoC: SELECT count(*) FROM expected_table, (SELECT * FROM users) AS leak with source_system resolving to expected_table. Its report claims _from_clause_tables "skips subqueries entirely" because the comma-list branch has if stripped_candidate.startswith("("): continue, so the users table inside the derived table allegedly never reaches the allowlist check.

That's not what the code does — the continue only stops the derived table's own alias blob from being misread as a literal table name in that branch. The nested FROM users is still independently found and checked, because the outer loop (for from_match in re.finditer(r"\bfrom\b", sql, ...)) scans every FROM occurrence in the whole string, including ones inside subqueries — not just the outermost one. Ran it rather than just tracing it:

>>> _from_clause_tables("SELECT count(*) FROM expected_table, (SELECT * FROM users) AS leak")
['expected_table', 'users']
>>> validate_sql_query(..., source_system="expected_table")
['unauthorized_table_reference']

Also checked the derived table ordered first, and multiple stacked derived tables ((SELECT a FROM users) leak1, (SELECT b FROM secrets) leak2, expected_table) — every smuggled table is caught in every ordering.

Separately, the sub-agent I used to extract this report from the job log noted the run's own summary box says a fix "was implemented and tested" while the code snippet it quotes as vulnerable is unchanged, and the job log shows exit code 2 / "Strix run failed" / "No Strix vulnerability report artifact was produced" — so this run may not have completed cleanly to begin with, independent of the analysis being wrong.

No code change from me on this one either — the exact PoC cited is already blocked, empirically, not just by inspection.


Generated by Claude Code

The nested-WHERE outer-relation PoC is already blocked by the depth-aware
FROM/JOIN scan, but skipping every '(' candidate omitted parenthesized
joined tables that have no inner FROM. FROM crm, (customer JOIN secrets
ON ...) therefore passed with only {crm}. Unwrap non-SELECT paren groups
with the existing JOIN/comma walker, reject unbalanced SQL, and cover
nested/CTE/OUTER/comma/paren-join adversarial cases.

Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

@coderabbitai review

Parse [project].dependencies via tomllib so the cryptography floor is an
actual metadata item, not a raw-text substring, and reject stale affected
pins in pyproject.toml and requirements-test.in separately. Translate the
existing dependency-security decision doc into the repo's Korean + English
technical-term style. No lock or pin changes.

Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
@cursor

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown

@coderabbitai review

@seonghobae

Copy link
Copy Markdown
Contributor Author

The latest OpenCode CHANGES_REQUESTED (head 4c12f25, coverage-evidence failure) is superseded on current head 9c12f5d (previous SQL fix b9a5b08). Same-head required checks on b9a5b08 were already green, including coverage-evidence, coverage-source-tree, opencode-review, Semgrep, Strix, Tests, CodeQL, and Security Scan (osv-scan + trivy-fs).

The earlier derived-table false positive (SELECT count(*) AS c FROM (SELECT a, b FROM crm) t) is covered by test_validate_sql_query_accepts_projection_commas_in_derived_table and remains allowed. Nested-WHERE and parenthesized-join smuggles fail closed.

No new product-code change in this note; 9c12f5d only answers the remaining CodeRabbit comments on files already in this PR (Korean doc prose + tomllib pin assertion). Locks/pins unchanged.

Copy link
Copy Markdown
Contributor Author

Exact-current-head review request for 9c12f5d760f618348d2f5c8742659168a3f8b2f3 over main@e48aa13c4af7a4875d4b53e6a60b50405c265a2f.

Fresh repository evidence on this unchanged head is terminal-success for Tests (32089298967), SAST Semgrep (32089298983), Security Scan (32089298987), and fuzz (32089298951). The earlier nested-subquery/table-smuggling concern is now addressed in source by depth-zero FROM/clause/JOIN/ON/comma scanning, balanced-parenthesis fail-closed validation, and parenthesized-join handling; review the current code rather than predecessor findings.

Review read-only and current-head-only. Do not mutate the branch, transfer predecessor evidence, self-approve for the author, weaken protection, or merge.

@opencode-agent review
@coderabbitai review
@cwl-noema-review review
@strix review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

@seonghobae I will review the current head only. I will not modify the branch, approve the pull request, change protections, or merge.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
tests/test_orchestrator.py (1)

89-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

리뷰 도구 이름 대신 동작을 나타내는 테스트 이름을 사용하십시오.

test_validate_sql_query_flags_coderabbit_nested_where_outer_relation은 리뷰 도구 이름을 포함합니다. 이 테스트가 검증하는 동작은 line 72의 테스트와 같으며 allowlist 대상만 다릅니다. 이름을 ..._flags_nested_where_with_allowlisted_inner_table 형태로 바꾸거나, 두 케이스를 pytest.mark.parametrize로 합치면 의도가 더 분명해집니다.

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@tests/test_orchestrator.py` around lines 89 - 104, Rename
test_validate_sql_query_flags_coderabbit_nested_where_outer_relation to a
behavior-focused name such as
test_validate_sql_query_flags_nested_where_with_allowlisted_inner_table,
removing the review-tool reference while preserving the existing assertions and
test coverage.
tests/test_api.py (1)

1543-1560: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

거부 결과의 audit event도 함께 검증하십시오.

이 테스트는 400 응답과 warning만 확인합니다. line 1487의 test_browse_query_rejects_table_outside_dataset_binding/audit/events에서 browse.query / rejected / query_safety_validation_failed 이벤트까지 확인합니다. browse/query 경로는 decision과 audit event를 evidence store에 기록해야 하므로, 괄호 조인 거부 케이스에도 같은 검증을 추가하면 evidence 기록 회귀를 잡을 수 있습니다.

♻️ 제안 diff
     assert response.status_code == 400
     assert "unauthorized_table_reference" in response.json()["detail"]["warnings"]
+
+    events = client.get("/audit/events", params={"resource": "crm-event"})
+    assert events.status_code == 200
+    assert any(
+        event["action"] == "browse.query"
+        and event["result"] == "rejected"
+        and event["reason"] == "query_safety_validation_failed"
+        for event in events.json()
+    )

위 제안은 "카탈로그 mutation ... 과 browse/query 경로는 데이터 접근 전에 policy.evaluate()를 수행하고, allow/deny decision과 audit event를 evidence store에 기록"한다는 가이드라인에 근거합니다.

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@tests/test_api.py` around lines 1543 - 1560, Extend
test_browse_query_rejects_parenthesized_join_unauthorized_table to query
/audit/events after asserting the 400 response and warning, and verify a
browse.query rejected event with reason query_safety_validation_failed, matching
the assertions in test_browse_query_rejects_table_outside_dataset_binding.

Source: Coding guidelines

src/sdp/orchestrator.py (1)

222-223: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

괄호 불균형에 전용 warning code 사용을 검토하십시오.

Line 222-223은 괄호 불균형을 unauthorized_table_reference로 보고합니다. fail-closed 동작은 올바릅니다. 그러나 이 warning 문자열은 execute_query의 audit event details와 API 400 응답 detail에 그대로 노출됩니다. 실제 원인이 파싱 실패일 때 "unauthorized table"이라는 진단은 운영자를 오도합니다.

malformed_sql_syntax 같은 별도 code를 추가하면 evidence 기록의 정확도가 올라갑니다. warning code는 API 계약이므로 테스트와 문서(README API 목록, docs/implementation-compliance.md)를 함께 갱신해야 합니다. 현재 계약을 유지하려면 이 항목은 그대로 두어도 됩니다.

위 판단은 "README의 API 목록과 구현 대응 표, 그리고 docs/implementation-compliance.md의 매트릭스는 기능 추가 시 함께 갱신한다" 가이드라인에 근거합니다.

Also applies to: 237-237

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@src/sdp/orchestrator.py` around lines 222 - 223, 괄호 불균형을 감지하는
_parentheses_balanced 분기에서 unauthorized_table_reference 대신 malformed_sql_syntax
전용 warning code를 사용하도록 업데이트하십시오. execute_query의 audit details와 API 400 응답이 새 코드를
반영하도록 확인하고, 관련 테스트와 README API 목록 및 구현 준수 매트릭스 문서도 함께 갱신하십시오.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Nitpick comments:
In `@src/sdp/orchestrator.py`:
- Around line 222-223: 괄호 불균형을 감지하는 _parentheses_balanced 분기에서
unauthorized_table_reference 대신 malformed_sql_syntax 전용 warning code를 사용하도록
업데이트하십시오. execute_query의 audit details와 API 400 응답이 새 코드를 반영하도록 확인하고, 관련 테스트와
README API 목록 및 구현 준수 매트릭스 문서도 함께 갱신하십시오.

In `@tests/test_api.py`:
- Around line 1543-1560: Extend
test_browse_query_rejects_parenthesized_join_unauthorized_table to query
/audit/events after asserting the 400 response and warning, and verify a
browse.query rejected event with reason query_safety_validation_failed, matching
the assertions in test_browse_query_rejects_table_outside_dataset_binding.

In `@tests/test_orchestrator.py`:
- Around line 89-104: Rename
test_validate_sql_query_flags_coderabbit_nested_where_outer_relation to a
behavior-focused name such as
test_validate_sql_query_flags_nested_where_with_allowlisted_inner_table,
removing the review-tool reference while preserving the existing assertions and
test coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 21c543ba-d02b-413e-b1d2-b8d37989ec42

📥 Commits

Reviewing files that changed from the base of the PR and between fcc5faa and 9c12f5d.

📒 Files selected for processing (5)
  • docs/doctoring/dependency-security.md
  • src/sdp/orchestrator.py
  • tests/test_api.py
  • tests/test_dependency_security.py
  • tests/test_orchestrator.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/doctoring/dependency-security.md
  • tests/test_dependency_security.py

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.

@opencode-agent opencode-agent Bot added area: data Database, schema, migration, ETL, or lineage priority: medium Normal-priority or P2 work status: needs-review Open pull request requiring current-head review or checks type: bug Defect or incorrect behavior labels Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: data Database, schema, migration, ETL, or lineage priority: medium Normal-priority or P2 work status: needs-review Open pull request requiring current-head review or checks type: bug Defect or incorrect behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants