UNPKG

miniml

Version:

A minimal, embeddable semantic data modeling language for generating SQL queries from YAML model definitions. Inspired by LookML.

44 lines (32 loc) 6.71 kB
# WAVE 1 — THE SECURITY & SUPPLY-CHAIN AUDITOR > Reconstructed brief. The dispatched subagent confirmed both headline findings (dead-code `validateAstSafety`; live nunjucks SSTI on `info`) before a session-limit API error terminated it mid-run. The Orchestrator re-verified both by executing probes, per the protocol's sequential-fallback clause. All [MEASURED] tags reflect commands actually run. ## Position (2 sentences) MiniML's entire value proposition is being the safety gate between an untrusted LLM and a production database, and that gate is partially inoperative: the advertised AST-based allowlist validation is dead code that never executes, so non-allowlisted SQL functions pass the WHERE/HAVING filter. The cruder regex layer still blocks subqueries, DDL/DML, and comments, so this is not a wide-open injection hole — but a security-conscious org cannot adopt a control whose primary advertised mechanism silently does nothing and whose test suite never noticed. ## Evidence (bypass attempts, dependency audit, supply-chain posture) ### Finding 1 — AST allowlist validation is DEAD CODE. Severity: HIGH. [MEASURED] `validation.ts:92` parses via `parser.astify("SELECT * FROM dummy WHERE " + expr)`. I confirmed against the installed `node-sql-parser` that a single statement yields an **object**, not an array. `validateAstSafety` only walks the AST inside `if (ast && Array.isArray(ast))` (line 280), so for every real input that branch is false and the function returns `{ok:true}` without inspecting anything. Executed bypass corpus against `validateWhereClause` (bigquery model, dims: revenue, customer_id): | Input | Result | If AST layer ran | |---|---|---| | `SESSION_USER() = revenue` | **ALLOWED** | should be BLOCKED (fn not in allowlist) | | `IFNULL(revenue,0) > 1` | ALLOWED | ALLOWED (control) | | `revenue = 1 OR 1=1` | ALLOWED | ALLOWED (tautology not in scope) | | `customer_id IN (SELECT id FROM t)` | BLOCKED | blocked by regex, not AST | | `CURRENT_USER = 'x'` | BLOCKED | blocked incidentally (parser quirk) | Impact: the allowlist that is supposed to permit "only safe functions" is not enforced. An LLM (or attacker steering it) can emit any function not on the hard-coded `DANGEROUS_CONSTRUCTS` keyword list — including dialect functions with real blast radius (e.g., BigQuery `EXTERNAL_QUERY`, `NET.*`, `ML.*`, or expensive UDFs), provided their arguments are model columns or literals. The regex layer's keyword blocklist is a denylist, not the advertised allowlist; denylists on SQL are historically porous. ### Finding 2 — nunjucks SSTI on `info`. Severity: MEDIUM (LOW if model files stay trusted). [MEASURED] `jinja.ts` runs `nunjucks.renderString(model.info, context)` with no sandbox; `load.ts:118` renders it at model-load. `{{7*7}}` renders `49` — template injection is live. Today `info` is model-authored and the security model designates YAML models as trusted, so this is defense-in-depth, not an open hole. But the project's own north star is multi-tenant conversational analytics and an MCP future where models may be generated or user-supplied; the moment untrusted text reaches `info`, this is server-side template injection with a path toward RCE (nunjucks templates expose JS object graph traversal). It should be sandboxed now, before that architecture arrives. ### Finding 3 — the trusted/untrusted boundary is real but under-enforced. [MEASURED] Date params are strictly regex-validated (`validateDateInput`, `YYYY-MM-DD`) before interpolation — good. But `order_by`, `limit`, `date_granularity`, and dimension/measure *names* are also attacker-controllable when an LLM fills them, and they are validated only by membership checks against model keys, not by the SQL validator. `limit` is numeric-guarded (`query.ts:160`). `order_by` keys are checked against model keys (`validateQueryInfo`). `date_granularity` is allowlisted in `dialect.ts:21`. So these are actually OK — the enforcement gap is specifically the WHERE/HAVING AST layer (Finding 1). ### Dependency & supply-chain audit. [MEASURED where noted / SPECULATION on maintenance] - 6 runtime deps: `yaml`, `nunjucks`, `he`, `word-wrap`, `cli-options`, `node-sql-parser`. `node-sql-parser` is Apache-2.0 and actively maintained; it is the security-critical dependency and it is well-chosen — the flaw is in how MiniML *calls* it, not the library. [MEASURED — read package.json + node_modules] - `cli-options` is an obscure, low-download package — a single-maintainer transitive-trust question worth noting for a post-xz reviewer. [SPECULATION on maintenance status — not independently verified this session] - **No CI, no publish provenance, no lockfile-verified release pipeline, single maintainer.** `.js`/`.d.ts` build artifacts are committed to the repo. For a package published to npm as a security control, the absence of CI + provenance + 2FA-attestation signals is itself an adoption blocker for a paranoid org. [MEASURED — no workflow files, artifacts present] ## Strongest point FOR the project The security *intent* and most of the perimeter are genuinely thoughtful: strict date-input regex, a real (if mis-wired) attempt at AST validation using the right library, a numeric limit guard, key-membership checks on every field parameter, and an explicitly stated trusted/untrusted boundary in the docs. This is materially more security design than a typical solo text-to-SQL wrapper, and the headline defect is a one-line fix (handle the object return at line 280), not an architectural dead-end. ## Strongest point AGAINST A security product whose flagship control is inert, whose 68 tests pass anyway because a weaker layer masks the failure, and which has no CI or release provenance, cannot be adopted by a security-conscious org as its LLM→SQL gate — the whole point is that you must be able to *trust the guarantee*, and here the advertised guarantee is provably not the one running. Discovering this took one probe; a hostile reviewer would close the tab. ## Viability score: 28/100 The idea is adoptable and the fixes are cheap, but as *shipped today* it fails its own core promise, which for a security-positioned library is close to disqualifying until fixed and proven. ## The one question that would change my mind After fixing the object-branch bug, does a red-team test corpus (100+ hostile WHERE/HAVING expressions across both dialects, plus a sandboxed-nunjucks assertion) pass in CI with the allowlist actually enforced? If yes, the perimeter is real and I rescore into the 50s; if the author can't or won't stand up that evidence, the "safe SQL for LLMs" claim should be removed from the README rather than trusted.