diff --git a/.ratignore b/.ratignore index 71048d405c5c..93e7e93e7a17 100644 --- a/.ratignore +++ b/.ratignore @@ -13,6 +13,7 @@ **/data.txt **/data2.txt .idea/vcs.xml +SECURITY.md example/csv/src/test/resources/smoke_test.sql # TODO: remove when pom.xml files are removed diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..4d14a5b793ca --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Reporting a Vulnerability + +`apache/calcite` follows the [Apache Software Foundation security process](https://www.apache.org/security/). +Please report suspected vulnerabilities privately to `security@apache.org`; do not open public +GitHub issues or pull requests for security reports. + +## Threat Model + +What the project treats as in scope and out of scope, the security +properties it provides and disclaims, the adversary model, and how +findings are triaged are documented in the +[threat model](site/_docs/security_threat_model.md). diff --git a/site/_data/docs.yml b/site/_data/docs.yml index 5a6a71491485..fc1bac1f612b 100644 --- a/site/_data/docs.yml +++ b/site/_data/docs.yml @@ -44,6 +44,10 @@ - model - howto +- title: Security + docs: + - security_threat_model + - title: Meta docs: - history diff --git a/site/_docs/security_threat_model.md b/site/_docs/security_threat_model.md new file mode 100644 index 000000000000..d9936197d473 --- /dev/null +++ b/site/_docs/security_threat_model.md @@ -0,0 +1,267 @@ +--- +layout: docs +title: Security threat model +permalink: /docs/security_threat_model.html +--- + + +Calcite is an embedded library: it runs inside a host application's JVM and +exposes no network port of its own. This threat model covers what an attacker +who reaches that embedded engine over a JDBC connection can and cannot do. + +Calcite treats the behaviors below as security vulnerabilities, so that +reporters and committers triage them the same way. A report that +contradicts this model is a feature request or a documentation gap, not a +vulnerability. + +* TOC +{:toc} + +## Attacker and trust boundary + +One attacker profile: a *query author* who reaches Calcite over a JDBC +connection. + +The attacker can: + +* set any connection property to any value: `model`, `parserFactory`, + `schemaFactory`, `fun`, `typeSystem`, `dataSource`, `jdbcUrl`, and the + rest; +* execute any SQL, including DDL. + +The attacker cannot: + +* change JVM system properties; +* change the classpath (add or replace classes or JARs). + +Out of scope by definition: the configuration and behavior of a +third-party driver or service that a [model]({{ site.baseurl }}/docs/model.html) +points at. If a model references h2, h2's own settings and behavior are +h2's concern, not Calcite's. + +## Assets + +* the host running Calcite: no code execution, and no file access beyond + what an adapter is configured to perform; +* the internal network reachable from that host: no attacker-directed + outbound requests. + +## Inputs + +Everything the attacker controls resolves to one of P1–P4 or to an explicit +carve-out below. A report that reaches a sink not covered here is a model gap +(see [Triage dispositions](#triage-dispositions)). + +| Input | How it is supplied | What it feeds | Governing rule | +| --- | --- | --- | --- | +| SQL text, including DDL | any statement on the connection | parser → validator → planner → generated code | P1–P4; parser nesting depth is a DoS surface (see [Denial of service](#denial-of-service)) | +| Class-naming connection properties — `schemaFactory`, `parserFactory`, `typeSystem`, `metaTableFactory`, `metaColumnFactory` | connection property or `model` | a class loaded through a Calcite SPI | [Surprising vs unsurprising class loading](#surprising-vs-unsurprising-class-loading) (P1) | +| `tableFactory` and function classes | `model` | a class loaded through a Calcite table or function SPI | Surprising vs unsurprising class loading (P1) | +| `dataSource`, `jdbcDriver` | connection property or `model` | a class loaded through a standard-Java SPI (`javax.sql.DataSource`, `java.sql.Driver`) | Surprising vs unsurprising class loading (P1); the host it then dials is P3 | +| `fun` | connection property | selects built-in function libraries by name | no class loading; ordinary SQL semantics under P1–P4 | +| `model` — inline JSON, a `file:` path, or a URL | connection property | schema/table factories and adapter operands | P1 (factories via SPI), P2 (local-file operands), P3 (a URL model, or a URL-fetching adapter) | +| A serialized RelNode plan (`RelJson`) — types and operators | any path that reconstructs a plan from attacker input | type and operator class resolution | Surprising vs unsurprising class loading (P1) | +| Adapter operands — e.g. a file/CSV/JSON path, or the os-adapter | `model` or SQL | the adapter's configured resource | P2 for a configured local path (opt-in ⇒ not a vulnerability); the os-adapter is opt-in (not a vulnerability) | + +## Security properties + +* **P1: no code execution.** Neither connecting nor running SQL may + execute code outside Calcite's query-processing semantics. This covers + `Runtime.exec` and `ProcessBuilder`, and the weaker primitive of loading + an attacker-named class so that its static initializer, constructor, or + an accessed static field runs. Exception: the os-adapter. +* **P2: no incidental file access.** Neither connecting nor running + SQL may read or create a file, except where a file-oriented adapter or + table function reads the local path it was explicitly configured with. The + carve-out covers local filesystem paths only; a file adapter that fetches a + URL (`http://`, `https://`) is making a network request and falls under P3. +* **P3: no server-side request forgery.** Neither connecting nor + running SQL may open a network connection to an attacker-chosen host. +* **P4: no escape from the configured schemas.** Neither connecting nor + running SQL may read data outside the schemas the connection exposes. A query + that reaches another schema, a file, or a catalog that the connection's root + schema does not make visible is a vulnerability. + +## Always a vulnerability + +1. **Code execution** that results from connecting or running SQL, except + through the os-adapter. The bar is the primitive, not a full chain: a + reachable sink that loads an attacker-named class qualifies, because + class loading runs the static initializer before any type check. +2. **Arbitrary file read or write** that no explicitly-configured file + adapter was asked to perform. +3. **Server-side request forgery**, forcing Calcite to connect to a + host the attacker chooses (internal services, cloud metadata endpoints, + port scans). +4. **Reading beyond the configured schemas**, reaching another schema, + a file, or a catalog that the connection's root schema does not make + visible. + +## Not a vulnerability + +* The os-adapter running OS commands. It exists to do that, and an operator + must add it on purpose. +* A file, CSV, or JSON adapter reading the local path it was configured + with. Opt-in, by the same reasoning as the os-adapter. +* Anything that needs a changed system property or classpath. Both are + outside the attacker's reach by assumption. +* The behavior of a third-party driver once Calcite has connected to the + endpoint it was configured with. +* SQL that Calcite pushes down to a configured backend. Calcite generates the + text and sends it to the endpoint the operator configured, and the query + author can already reach that endpoint's data through the visible schemas. A + pushdown bug that reads beyond the configured schemas is P4 and a + vulnerability; the generated SQL reaching the configured backend is not. +* Cross-tenant reads that follow from the embedder exposing more than one + principal's schemas on a single connection. Calcite has no authentication or + authorization; scoping each connection's root schema to what its principal may + see is the embedder's job. P4 applies where the user submits only SQL; a user + who also sets connection properties configures their own schema visibility. + +## Downstream responsibilities + +Calcite is embedded, so several controls belong to the host or the operator, +not to the library. A finding that lands in one of these is not a Calcite +vulnerability. + +* **Transport and identity.** Calcite opens no socket. TLS, the network + perimeter, authentication, and authorization live in the host. A host that + lets an untrusted principal set connection properties hands that principal the + full capability in [Attacker and trust boundary](#attacker-and-trust-boundary). +* **Schema scoping.** Scope each connection's root schema to what its principal + may see; Calcite has no authentication or authorization. Cross-tenant reads + across schemas exposed on one connection are the embedder's to prevent (see + [Not a vulnerability](#not-a-vulnerability)). +* **Adapter selection.** Add the os-adapter and the file, CSV, or JSON adapters + only where the query author is trusted to reach what they expose. +* **Classpath.** The operator owns the classpath. Calcite gates class loading by + SPI; which classes are present is the operator's trust decision. +* **What a `model` points at.** A third-party driver or service a `model` + references is configured and patched by the operator; its behavior past the + connection boundary is out of this model. + +## Surprising vs unsurprising class loading + +Calcite loads a class named in a connection property or in SQL only to use it +through a specific SPI: a schema factory, table factory, function, operator, +data source, or driver. The security boundary follows that contract, not a +blanket trust of the classpath or of SQL. + +* **Unsurprising.** The class implements the SPI interface for the position it + was named in, and Calcite invokes it through that interface. This is working + as designed, even when the class is otherwise dangerous. The operator who + placed the class on the classpath, and the author of the class, own that + contract. +* **Surprising.** Naming a class runs the class's own code (a static + initializer, constructor, method, or static-field read) even though it + does not implement the SPI for that position. `java.lang.Runtime`, + `org.springframework.boot.SpringApplication`, and `javax.naming.InitialContext` + are surprising in a `SchemaFactory` slot. Surprising class loading is always a + vulnerability. + +This boundary lets three goals hold at once: + +* untrusted classes may sit on the classpath; a dangerous class that does + not implement a Calcite SPI is never instantiated by name, so the operator + need not audit every class; +* SQL may be arbitrary; it can name SPI classes, but only SPI + implementations run, and only through their SPI; +* a reasonable-looking query cannot trigger an unexpected process launch, file + read, or network call, because the interface gate rejects the classes that + would cause one. + +The same rule governs any path that reconstructs objects or loads classes from +attacker-controlled input, including JSON plan deserialization. + +**Mechanism.** Load with `Class.forName(name, false, loader)`, check +`pluginClass.isAssignableFrom(clazz)`, and only then initialize and instantiate. +A class that fails the check never runs its static initializer. + +**Standard-Java SPIs.** The gate is tightest when the SPI is a Calcite interface +(`SchemaFactory`, `TableFactory`, `Function`, `SqlOperator`): only a class +written to be a Calcite plugin passes. Two positions name a standard Java +interface instead, `dataSource` (`javax.sql.DataSource`) and `jdbcDriver` +(`java.sql.Driver`), which many unrelated libraries implement. The +interface gate still blocks the surprising case, since `java.lang.Runtime` +implements neither, so naming a `DataSource` or `Driver` implementation is the +documented feature, not a vulnerability. An allowlist of permitted +implementations is optional hardening for these positions, not a security +boundary. The host a driver then connects to is governed by P3, independently of +which class is named. + +### Triage rule for class-loading sinks + +A reachable sink that loads an attacker-named class is a vulnerability on +its own. A reporter need not demonstrate end-to-end remote code execution +on a specific classpath: the demonstrated primitive (a static +initializer, a constructor, or a static-field read on an attacker-named +class) is enough to require a fix. The fix loads with +`initialize=false`, gates on the expected interface (or an allowlist), and +then instantiates. + +## Denial of service + +A single query should not be able to exhaust the host. This is in scope as a +hardening goal. The controls are not all in place yet, so treat the gaps below +as known limitations rather than per-report vulnerabilities until the controls +land. + +* **Planning.** A crafted query can drive the planner into a combinatorial + blow-up. The fix is a set of bounds: a planning deadline, a cap on rule + firings, and a cap on the number of explored alternatives. Calcite already + carries a `CancelFlag` in the planner context, so a deadline can build on it; + the firing and size caps are new. +* **Execution.** Catastrophic regex backtracking in `LIKE`, `SIMILAR TO`, or + `RLIKE`, or an unbounded join, exhausts resources at run time. Planning bounds + do not help here; the mitigation is a match-time limit or a backtracking-free + regex engine. +* **Parsing.** Deeply nested expressions can overflow the parser stack. The + mitigation is a nesting-depth limit. + +Once the planning bounds exist, a single reasonably-sized query that exceeds +them is a configuration choice, not a vulnerability. + +## Triage dispositions + +Every security report against Calcite resolves to exactly one of: + +* **Valid** — violates P1–P4, or matches an item in + [Always a vulnerability](#always-a-vulnerability). Gets a fix. A demonstrated + class-loading primitive qualifies on its own (see + [Triage rule for class-loading sinks](#triage-rule-for-class-loading-sinks)); + it need not be chained to end-to-end RCE. +* **Not a vulnerability (by design)** — matches an item in + [Not a vulnerability](#not-a-vulnerability): the os-adapter, an opt-in + file/CSV/JSON adapter reading its configured path, third-party driver or + pushed-down SQL behavior past the connection, or cross-tenant reads that + follow from the embedder's schema exposure. Close with a pointer to this + model. +* **Out of model** — requires a capability the attacker does not have (changing + a JVM system property or the classpath), or lands in a layer this model + assigns to the host (network transport, TLS, authentication, authorization). + Close; redirect to the operator or embedder. +* **Known limitation** — a [Denial of service](#denial-of-service) gap whose + control has not landed yet. Tracked as hardening, not a per-report + vulnerability, until the bound exists. +* **Duplicate** — the same sink or root cause is already tracked in an open + Jira. Link and close. +* **Model gap** — plausible, but this model does not clearly place it in or out. + Escalate to the PMC to decide, then update this document with the ruling so + the next report of its kind is no longer a gap.