Skip to content

fix(sinks): confinement follow-ups from #25820 codex review#25830

Draft
pront wants to merge 8 commits into
masterfrom
pront-confinement-followups
Draft

fix(sinks): confinement follow-ups from #25820 codex review#25830
pront wants to merge 8 commits into
masterfrom
pront-confinement-followups

Conversation

@pront

@pront pront commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

Addresses the 13 items surfaced in the Codex review of #25820 (.claude/investigations/pr25820-confinement-followups.md from that branch). This PR is stacked on pront-sink-template-confinement — do not merge until #25820 lands.

Twelve items are addressed in code; one (Splunk HEC process_log bypass) is tracked as an issue: #25829.

Landed

Attacker-controlled routing gaps (P1):

  • Loki: labels + structured_metadata templates now confined (build-time + drop on Confined).
  • Elasticsearch auto_routing: event-side data_stream.{type,dataset,namespace} values run through the corresponding template's confinement check.
  • URI query prefix injection: UriChecker now records and enforces the static query prefix, so ?tenant=team-{{ tenant }} + a&tenant=other no longer slips through.

Correctness gaps (P2):

  • TemplateRenderingError::Confined now emits error_type=condition_failed on log + counter (matches the security changelog).
  • Partial URI authority (https://logs-{{ region }}.example.com/...) rejected at build time rather than dropping 100% of rendered events.
  • URI vs prefix checker now selected by caller intent (Template::confine_uri) rather than inferring from whether the literal prefix starts with http. Object-store keys that happen to start with http:// no longer get URI-checked.
  • Stackdriver: resource.labels and label_config.labels templates confined.
  • HTTP: templated header values confined (mirrors URI confinement).
  • File sink: base_dir now enforced even when path is static.

Usability regressions (P2):

  • Humio: HumioLogsConfig / HumioMetricsConfig now expose dangerously_allow_unconfined_template_resolution and forward it to the wrapped HEC build.
  • WebHDFS: root composed into the partitioner key template so root: /logs + prefix: "{{ tenant }}/" is properly confined by /logs/.
  • Elasticsearch: bulk.index no longer confined when mode: data_stream, and vice versa.

Deferred

Change Type

  • Bug fix
  • New feature
  • Dependencies
  • Non-functional (chore, refactoring, docs)
  • Performance

Is this a breaking change?

  • Yes
  • No

Config changes are strictly additive (new dangerously_allow_unconfined_template_resolution on Humio sinks, no other externally-visible surface changes).

Does this PR include user facing changes?

  • Yes. Please add a changelog fragment based on our guidelines.
  • No. A maintainer will apply the no-changelog label to this PR.

The user-facing security story is covered by #25820's .security fragment; these are follow-up hardening on the same landing.

References

@github-actions github-actions Bot added docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. domain: sources Anything related to the Vector's sources domain: sinks Anything related to the Vector's sinks domain: external docs Anything related to Vector's external, public documentation labels Jul 13, 2026
@datadog-vectordotdev

datadog-vectordotdev Bot commented Jul 13, 2026

Copy link
Copy Markdown

Pipelines  Tests

⚠️ Warnings

🚦 2 Pipeline jobs failed

Test Suite | Check events   View in Datadog   GitHub Actions

Test Suite | Test Suite   View in Datadog   GitHub Actions

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: ad29b48 | Docs | Give us feedback!

@pront pront added the no-changelog Changes in this PR do not need user-facing explanations in the release changelog label Jul 13, 2026
pront added 4 commits July 13, 2026 20:08
Previously only tenant_id was confined; label key/value and
structured_metadata key/value templates were rendered directly, so an
event whose data controlled a templated label could steer to any Loki
stream even after this PR's confinement work.

Both maps now have confine() applied to each key and value template at
build time, and the render paths return an Err(()) that encode_event
translates into an intentional security drop when either key or value
render fails with Confined.
Under `auto_routing: true` (the default), DataStreamConfig::index reads
data_stream.{type,dataset,namespace} directly off the event, completely
bypassing the confined templates. A log producer that controls those
event fields could steer to arbitrary data streams even after this PR's
confinement work.

Add Template::check_confinement(rendered) so callers that skip normal
template rendering (like the auto-routing path) can still run the
attached checker against a raw string. Elasticsearch's auto-routing
branch now runs the pulled event value through the corresponding
template's confinement check and drops the event on Confined.
When the operator-authored template for data_stream.{type,dataset,namespace}
is static (e.g. the default type = "logs"), no ConfinementChecker gets
attached and Template::check_confinement returns Ok for any raw event
value. So under auto_routing: true (the default), an attacker-controlled
event field like data_stream.type: "../evil" bypassed the check even
though the goal of the previous auto_routing fix was to plug exactly that
hole.

Add a baseline is_valid_data_stream_component check that rejects any
value containing path separators, .. or . segments, NUL bytes, control
characters, or exceeding Elasticsearch's 255-byte name limit. Applied to
every auto-routed value regardless of whether the corresponding template
has a checker attached.
UriChecker had no query handling. Two attack surfaces:

1. Build: a template like ?tenant={{ tenant }} lets an attacker
   supply a value containing & to append arbitrary key=value pairs.
   Every attempt to reason about & boundaries and multi-field
   patterns was either bypassable or false-positive on operator-
   authored static suffixes. Reject any URI template whose literal
   prefix crosses into the query at build time; operators can put
   dynamic routing in the path instead.

2. Render: with the build check in place, a URI checker only exists
   for pathful templates. So any rendered URI with a query means a
   field value smuggled ?... into the path. Example: template
   .../ingest/{{ tenant }} with tenant = 'ok?tenant=evil' renders
   to .../ingest/ok?tenant=evil — same authority, same path prefix,
   attacker-controlled query. Reject any rendered URI with a
   non-empty query.
@pront pront force-pushed the pront-confinement-followups branch from da2f05d to c3ba20a Compare July 14, 2026 00:17
@github-actions github-actions Bot removed domain: sources Anything related to the Vector's sources domain: external docs Anything related to Vector's external, public documentation labels Jul 14, 2026
pront added 4 commits July 13, 2026 20:44
…fig.labels templates

PR #25820 confined `log_id` only. `resource.labels` and
`label_config.labels` templates were cloned into the encoder unchanged,
so an event-controlled label value (e.g. `resource.labels.zone:
"{{ zone }}"`) could still steer the destination monitored resource.

Confine every label value template at build time. Render-time drops
already fall through the encoder's existing `.ok()?` path with
`drop_event: true`, so `Confined` renders map to intentional drops
via the parent PR's error_type wiring.
PR #25820 confined the URI but left `template_headers` (dynamic
values from `RequestConfig::split_headers`) unchecked. Header-based
routing like `X-Scope-OrgID: "{{ tenant }}"` is as steerable as
URI routing — an event that controls the header field picks the
destination tenant. Run `.confine()` on each templated header
value at build time.
…utates event

process_log extracts and removes fields (like `timestamp`) from the
event body. Running the confinement pre-check *after* process_log
turned a Confined render into MissingKeys when the confinement error
came from a field process_log had just removed — the security drop
was silently downgraded to 'field missing', and the attack event
shipped without an index.

Example under `index: "idx-{{ timestamp }}"`: an event with
`timestamp = "../../evil"` used to render as Confined inside
process_log, then process_log removed the timestamp field, then the
pre-check re-rendered and saw MissingKeys instead of Confined.

Move the pre-check to run on the original Event before process_log,
and change has_confined_partition_error to take an `&Event` rather
than an `&HecProcessedEvent`. Adds a regression test that reproduces
the bypass shape.

Closes #25829.
TemplateRenderingError previously hard-coded error_type=template_failed
for every render failure. Sinks that surface confinement via this event
(Kafka, Redis, HTTP URI, S3 KMS key, Stackdriver labels, HTTP headers,
Splunk HEC pre-check) therefore all emitted 'template_failed' for
confinement drops. Operators alerting on error_type='condition_failed'
per the parent PR's changelog missed every one of them.

Branch on the underlying error variant: Confined maps to
error_type='condition_failed' with a message wording that identifies
the class ('Templated routing value was outside the configured
confinement; dropping event.'), everything else keeps the previous
'template_failed'/'Failed to render template.' pair. Ordinary render
failures (MissingKeys, NotNumeric) are legitimate pre-existing drops
and stay tagged as template_failed.

Also amends the parent PR's still-unreleased security changelog
fragment to make the observability contract accurate: condition_failed
is the canonical filter for confinement drops specifically, not every
drop. Message wording is per-sink but the label is consistent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. domain: sinks Anything related to the Vector's sinks no-changelog Changes in this PR do not need user-facing explanations in the release changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant