You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(saved-objects): guard against source commands in ComposerQuery pipeline
Restores the runtime check that rejects pipelines starting with FROM, ROW,
SHOW, METRICS, or TS. The check now runs against toRequest().query rather
than the raw string, since pipeline is now always a ComposerQuery.
Without this a developer writing esql`FROM .kibana | LIMIT 10` would get
an opaque ES|QL syntax error from Elasticsearch instead of a clear message.
Co-authored-by: Cursor <cursoragent@cursor.com>
`SavedObjectsClientContract.esql` allows you to query Saved Objects using [ES|QL (Elasticsearch Query Language)](https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql). It returns tabular results (columns and values) directly from Elasticsearch, which can be useful for analytics, aggregations, and cross-type queries that don't fit the `find` or `search` methods.
11
-
12
-
## Relationship to `find` and `search`
10
+
`SavedObjectsClientContract.esql` allows you to query Saved Objects using [ES|QL (Elasticsearch Query Language)](https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql). It returns tabular results (columns and values) directly from Elasticsearch, useful for analytics, aggregations, and cross-type queries that don't fit `find` or `search`.
13
11
14
12
| Method | Use case | Response format |
15
13
|--------|----------|----------------|
16
-
|`find`| Simple filtering and pagination of saved objects | Structured `SavedObject[]`|
17
-
|`search`| Complex queries using Elasticsearch Query DSL | Raw Elasticsearch search hits |
<DocCallOuttitle="With great power comes great responsibility">
23
-
While the `esql` method is powerful, it can increase code complexity, introduce performance issues and introduce security risks (like injection attacks). Carefully consider how you would like to use this method in your plugin to unlock value for users.
19
+
The `esql` method gives direct access to saved object indices and executes with elevated `kibana_system` privileges. Always construct pipelines server-side and use parameterized values for any user-supplied input.
24
20
</DocCallOut>
25
21
26
-
## The `pipeline` concept
22
+
## Basic usage
27
23
28
-
Like `search` and `find`, you specify saved object **types**as a dedicated parameter — you never need to know or write index names. The `esql` method resolves the correct Elasticsearch indices from the `type` parameter and auto-generates the `FROM` clause. Security filters (namespace + type restriction) are injected via the `filter` parameter, so you don't need `WHERE type ==` either.
24
+
You specify saved object **types**— never index names. The `FROM` clause, namespace filter, and type restriction are all injected automatically. You write only the ES|QL **processing pipeline**.
29
25
30
-
You write only the ES|QL **processing pipeline** — everything after `FROM`:
26
+
Build the pipeline with the `esql` tagged template from `@elastic/esql`:
The `esql` tag accepts a pipeline-only template (no `FROM` required). It temporarily wraps the
94
-
template in `FROM a | …` to satisfy the parser, then strips the source command, leaving a
95
-
`ComposerQuery` whose `.toRequest()` returns the `?param` placeholder string alongside a
96
-
`params` array. The SO client calls `.toRequest()` internally.
97
-
98
-
You can also mix composer params with a raw `params` array — they are merged before the request
99
-
is sent, with composer params first.
100
-
101
-
## Safe pipeline construction with ES|QL params
102
-
103
-
When interpolating user input into ES|QL pipelines, **never** use string concatenation. Instead, use ES|QL's native parameterization — named params (`?paramName`) or positional params (`?`) — to separate code from data at the protocol level.
58
+
See the full example in the Kibana repository at `examples/saved_objects`.
104
59
105
-
### Named params `?paramName` (recommended for user input)
60
+
##Handling user input safely
106
61
107
-
Use `?paramName` placeholders in the pipeline string and pass the values via the `params` array as `{ name: value }` entries. This is true parameterization — the values are never interpolated into the query string, preventing injection attacks.
62
+
Use `${{ name: value }}` holes in the `esql` template for any user-supplied value. The value becomes a named ES|QL parameter — it is forwarded to Elasticsearch separately and never appears in the query string.
The pipeline sent to Elasticsearch will be `| WHERE my_type.title LIKE ?searchTerm | LIMIT 100` — with `searchTerm` as a separate parameter, never interpolated into the pipeline string.
125
-
126
-
### Positional params `?` (alternative)
127
-
128
-
ES|QL also supports positional `?` placeholders. Params are plain values (string, number, boolean, or null) matched by position:
129
-
130
-
```ts
131
-
const result =awaitsavedObjectsClient.esql({
132
-
type: ['my_type'],
133
-
namespaces: ['default'],
134
-
pipeline: '| WHERE my_type.title LIKE ? | LIMIT 100',
135
-
params: [userInput],
136
-
});
137
-
```
78
+
**Never pass user input via `esql.exp(userInput)`** — that injects a raw ES|QL expression and bypasses parameterization entirely.
138
79
139
80
## Security model
140
81
141
-
### Execution context — `kibana_system` user
142
-
143
-
The ES|QL pipeline executes with the privileges of the `kibana_system` Elasticsearch user, which has elevated access including `manage_enrich` and broad index monitoring permissions. This means the pipeline can access resources that the end user may not be authorized to see.
144
-
145
-
**Never inject arbitrary or untrusted user input directly into the pipeline string.** If a user can control the full pipeline, they could use commands like `ENRICH` to join against enrich policies whose source data they would not normally have access to — this is a privilege escalation. Always construct the pipeline server-side and use parameterized values (see [Safe pipeline construction](#safe-pipeline-construction-with-esql-params)) for any user-provided input.
146
-
147
-
Using `ENRICH` in your pipeline is perfectly fine when you control the pipeline and are enriching from a policy whose data is appropriate for all users who will see the results.
148
-
149
-
### Index resolution from types
82
+
### Execution context
150
83
151
-
Like `search` (which passes `index: getIndicesForTypes(types)` internally), the `esql` method resolves the correct Elasticsearch indices from the `type` parameter and auto-generates the `FROM` clause. You never need to know the index name — it is an implementation detail handled by the saved objects system.
84
+
The pipeline executes as the `kibana_system` Elasticsearch user, which has elevated privileges. A user who controls the pipeline could use `ENRICH` to access data they are not authorized to see. Always construct the pipeline server-side; never let user input determine the pipeline structure.
152
85
153
86
### Space and type filtering
154
87
155
-
When you call `esql()`, namespace (space) and type filters are automatically injected into the `filter` parameter of the ES|QL request. The filter restricts results to the specified types and namespaces, so you don't need `WHERE type == "..."` in your pipeline. This works the same way as the `search` method:
88
+
Namespace and type filters are automatically injected into every request. You never need `WHERE type == "..."` in your pipeline. If the caller is not authorized to access the requested namespaces or types, an empty response is returned.
156
89
157
-
1.`spacesExtension.getSearchableNamespaces()` resolves which namespaces the user can access
3. A namespace bool filter (including type restriction) is constructed and merged with any user-provided `filter`
90
+
### User-provided filters
160
91
161
-
If the user is not authorized to access any of the requested namespaces or types, an empty response is returned.
162
-
163
-
### User-provided filters are merged, not replaced
164
-
165
-
If you provide a `filter` in the options, it is merged with the security filter using `{ bool: { must: [securityFilter, yourFilter] } }`. Your filter is never used in isolation.
92
+
If you supply a `filter` option it is merged with the security filter as `{ bool: { must: [securityFilter, yourFilter] } }` — never used in isolation.
166
93
167
94
## Encrypted attributes
168
95
169
-
Encrypted saved object attributes are handled differently depending on whether `_source` is present in the response:
170
-
171
-
-**With `_source` (via `metadata: ['_id', '_source']`):** The full document in `_source` contains all attributes needed for AAD (Additional Authenticated Data) reconstruction. Encrypted attributes are by default stripped from `_source`, or are **decrypted** in `_source`, using the same path as `find` and `search`, if registered with the `dangerouslyExposeValue` option. If decryption fails (e.g., key rotation), all encrypted attributes are stripped from `_source`.
172
-
-**Standalone scalar columns (e.g., `connector.secrets`):** Always replaced with `null`, regardless of `_source` decryption. These columns contain raw ciphertext that cannot be used outside the document context.
173
-
174
-
For example, if a `connector` type has an encrypted `secrets` attribute:
175
-
-`connector.secrets` column → always `null`
176
-
-`_source` column → contains the full document with `secrets` decrypted (or stripped on failure)
96
+
-**Standalone scalar columns** (e.g. `connector.secrets`): always replaced with `null`.
97
+
-**`_source` column** (requires `metadata: ['_id', '_source']`): encrypted attributes are decrypted using the same path as `find` and `search`. If decryption fails, encrypted attributes are stripped from that row's `_source`.
177
98
178
99
## Response structure
179
100
180
-
The `esql` method returns the raw ES|QL response with `columns` and `values`:
181
-
182
101
```json
183
102
{
184
103
"columns": [
@@ -194,13 +113,12 @@ The `esql` method returns the raw ES|QL response with `columns` and `values`:
194
113
195
114
## When to use
196
115
197
-
-You need ES|QL-specific operations like `STATS`, `EVAL`, `ENRICH`, `DISSECT`, or`GROK`
198
-
-You want tabular results for analytics or reporting
199
-
-You need to compute aggregations across saved object types
0 commit comments