Skip to content

fix: audit #23 — DDL asSelect routes through compile() (plugin-walk)#88

Merged
productdevbook merged 2 commits into
mainfrom
fix/audit23
Apr 21, 2026
Merged

fix: audit #23 — DDL asSelect routes through compile() (plugin-walk)#88
productdevbook merged 2 commits into
mainfrom
fix/audit23

Conversation

@productdevbook
Copy link
Copy Markdown
Owner

Summary

HIGH (SECURITY)CREATE TABLE … AS SELECT and CREATE VIEW … AS SELECT bypassed every plugin.

Sumak.compile() routed DDL nodes directly to DDLPrinter, wiring the DDL printer's inner-SELECT callback to the raw dialect printer (base.print(sel)). That path skipped every pipeline stage — plugin transformNode, hooks, normalize, optimize. A CREATE VIEW tenant_orders AS SELECT * FROM orders on a multi-tenant schema captured every tenant's rows into the view. MultiTenantPlugin had no chance to attach its tenant filter; SoftDeletePlugin couldn't mask deleted rows.

Fix: wire the DDL printer's SELECT callback to this.compile(sel) instead of base.print(sel) at all three call sites (compile(), generateDDL(), compileDDL()). The inner SELECT now goes through the full 7-stage pipeline.

Test plan

  • pnpm fmt && pnpm lint && pnpm typecheck clean
  • pnpm test — 1239 pass (+4 new)
  • Reproducer confirmed: CREATE TABLE orders_cache AS (SELECT * FROM orders WHERE ("tenant_id" = $1)) with params [42]
  • Regression tests cover CTAS + CREATE VIEW with MultiTenant and SoftDelete plugins, plus a no-plugin baseline

🤖 Generated with Claude Code

productdevbook and others added 2 commits April 21, 2026 12:53
HIGH (SECURITY) — CREATE TABLE AS SELECT / CREATE VIEW AS SELECT
bypassed plugins entirely.

When the Sumak.compile() router hit a DDL node it wired the DDL
printer's inner-SELECT callback to the raw dialect printer (`base.print(sel)`).
That path skipped every pipeline stage: plugin transformNode, hooks,
normalize, optimize. A `CREATE VIEW tenant_orders AS SELECT * FROM
orders` captured every tenant's rows into the view — MultiTenantPlugin
had no chance to attach its tenant filter. Same for SoftDeletePlugin
(deleted rows leaked into the materialized copy), AuditTimestampPlugin
(stale views), OptimisticLockPlugin.

Fix: wire the DDL printer's SELECT callback to `this.compile(sel)`
instead of `base.print(sel)`, at all three call sites (compile(),
generateDDL(), compileDDL()). The inner SELECT now goes through the
full 7-stage pipeline just like a top-level query.

Regression tests cover CTAS + CREATE VIEW with MultiTenant and
SoftDelete plugins, plus a no-plugin baseline to confirm the no-op
path still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review of PR #88 noted the shared DDLPrinter across the generateDDL
loop is inconsistent with the one-printer-per-statement contract
used by compileDDL(). While print()'s internal params reset makes
the shared instance safe for the current schema-only code path,
future additions (asSelect on DDL, raw-SQL defaults accumulating
params in printExpr) would have to remember the invariant. Move the
printer allocation inside the loop to match compileDDL.

No observable behavior change with current inputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@productdevbook productdevbook merged commit faf3787 into main Apr 21, 2026
1 check passed
@productdevbook productdevbook deleted the fix/audit23 branch April 21, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant