Commit 38f42e7
authored
[exporter/opensearchexporter] Validate attribute values in dynamic index names (#49362)
#### Description
Validates attribute values before they are substituted into a
`%{placeholder}` index name in the dynamic index resolver to mitigate
#49225.
Rejecting leading-dot (`.`) and (`..`) keeps a value from resolving to a
system index (e.g. `.kibana`) or a different index via a `..` segment. A
value that is invalid to OpenSearch (uppercase, spaces, forbidden
characters) remains untouched and surfaces as an index-time
(server-side) error rather than being silently rerouted to the
configured fallback.
The leading-dot escalation requires the placeholder to lead the index
name (e.g. `traces_index: "%{tenant}"`); a static prefix like
`logs-%{tenant}` already keeps the result out of the system namespace.
**What this does not cover.** A well-formed but unauthorized value still
resolves. With `traces_index: "%{tenant}"`, an attacker setting
`tenant=team-b` still produces the `team-b` index, because `team-b` is a
valid index segment. Character validation can't distinguish an
authorized tenant from an unauthorized one; cross-tenant isolation needs
tenant allowlisting or an immutable prefix.
#### Link to tracking issue
- related to #49225.
#### Testing
- `go test ./...` in `exporter/opensearchexporter/`.
- New `index_resolver_test.go` cases cover path traversal
(`../../system`), `..`, leading dot, values substituted verbatim (`/`,
`\`, space, uppercase), and fall-through from an invalid
higher-precedence attribute to a valid lower-precedence one.
- Verified end-to-end against OpenSearch 2.17.1 and 3.7.0 (shell output
below).
<details>
<summary>Full repro steps</summary>
Two OpenSearch nodes (security plugin off for plain HTTP):
```yaml
# docker-compose.yaml
services:
opensearch2:
image: opensearchproject/opensearch:2.17.1
environment: [discovery.type=single-node, "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m", DISABLE_SECURITY_PLUGIN=true, DISABLE_INSTALL_DEMO_CONFIG=true]
ports: ["9201:9200"]
opensearch3:
image: opensearchproject/opensearch:3.7.0
environment: [discovery.type=single-node, "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m", DISABLE_SECURITY_PLUGIN=true, DISABLE_INSTALL_DEMO_CONFIG=true]
ports: ["9202:9200"]
```
```sh
docker-compose up -d
```
Build a collector with this branch's exporter. `make otelcontribcol`
from the repo root is the official path (its `genotelcontribcol` step
adds a local `replace` for every module). For faster single-exporter
iteration, a minimal module wiring the OTLP receiver, debug exporter,
and this exporter with a `replace` to the local tree works too.
Collector config. `traces_index: "%{tenant}"` makes the index name fully
attacker-controlled, which is what exposes the leading-dot system-index
case:
```yaml
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4328
exporters:
debug: { verbosity: basic }
opensearch:
http:
endpoint: http://localhost:9202 # or :9201 for the 2.x node
traces_index: "%{tenant}"
traces_index_fallback: "rejected-traces"
mapping:
mode: ss4o
service:
pipelines:
traces:
receivers: [otlp]
exporters: [debug, opensearch]
```
Send a span carrying the `tenant` value under test (swap the value per
case):
```json
{
"resourceSpans": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "victim" } },
{ "key": "tenant", "value": { "stringValue": "../../system" } }
]
},
"scopeSpans": [
{
"scope": { "name": "repro" },
"spans": [
{
"traceId": "5b8efff798038103d269b633813fc60c",
"spanId": "eee19b7ec3c1b174",
"name": "span",
"kind": 1,
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000000000000010"
}
]
}
]
}
]
}
```
Send the span (swap the `tenant` value per case), then list indices:
```sh
curl -s -X POST http://localhost:4328/v1/traces -H 'Content-Type: application/json' -d @span.json
curl -s "http://localhost:9202/_cat/indices?h=index,docs.count"
```
Sending `team-a`, `team-b`, `.kibana_x`, `../../system` in turn, without
the fix:
```text
tenant=team-a HTTP 200
tenant=team-b HTTP 200
tenant=.kibana_x HTTP 200
tenant=../../system HTTP 500
-- indices --
.kibana_x 1 # leading dot created a system-namespace index
team-a 1
team-b 1 # cross-tenant write
```
`../../system` returns HTTP 500; the exporter logs
`invalid_index_name_exception` (the `/` is a forbidden index char) and
drops the span. The same four with the fix:
```text
tenant=team-a HTTP 200
tenant=team-b HTTP 200
tenant=.kibana_x HTTP 200
tenant=../../system HTTP 200
-- indices --
rejected-traces 2 # .kibana_x and ../../system both routed here
team-a 1
team-b 1 # still resolves: cross-tenant gap (see #49225)
```
</details>
#### Documentation
- README dynamic-indexing section documents the value restriction and
fall-through behavior.
- `.chloggen/opensearchexporter-sanitize-index-placeholders.yaml` added.
---
- [x] I, a human, wrote this pull request description myself.1 parent bb9c3e4 commit 38f42e7
4 files changed
Lines changed: 116 additions & 3 deletions
File tree
- .chloggen
- exporter/opensearchexporter
Lines changed: 32 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
106 | | - | |
| 106 | + | |
107 | 107 | | |
108 | 108 | | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | | - | |
| 112 | + | |
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
132 | 144 | | |
133 | 145 | | |
134 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
141 | 209 | | |
142 | 210 | | |
143 | 211 | | |
| |||
0 commit comments