Skip to main content

vector/template/
mod.rs

1//! Functionality for managing template fields used by Vector's sinks.
2mod configurable;
3mod confined;
4mod confinement;
5mod parsing;
6mod unconfined;
7mod unsigned;
8
9use std::{borrow::Cow, convert::TryFrom, fmt, hash::Hash, path::PathBuf, sync::LazyLock};
10
11use bytes::Bytes;
12use chrono::{
13    FixedOffset, Utc,
14    format::{Item, strftime::StrftimeItems},
15};
16use http::Uri;
17use regex::Regex;
18
19use snafu::Snafu;
20use tracing::warn;
21use vector_lib::{
22    configurable::{ConfigurableNumber, ConfigurableString, NumberClass, configurable_component},
23    lookup::lookup_v2::parse_target_path,
24};
25
26use crate::{
27    config::log_schema,
28    event::{EventRef, Metric, Value},
29};
30
31use confinement::ConfinementChecker;
32use parsing::Part;
33use unsigned::UnsignedIntTemplateSource;
34
35#[cfg(test)]
36use confinement::{BuildError, ConfineError};
37
38/// Maximum byte length of a rendered value before it is rejected.
39///
40/// Bounds per-event cost and provides a coarse cap on memory blow-up from
41/// attacker-controlled fields.
42pub const MAX_RENDERED_PATH_LEN: usize = 1024;
43
44static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\{(?P<key>[^\}]+)\}\}").unwrap());
45
46/// Errors raised whilst parsing a Template field.
47#[allow(missing_docs)]
48#[derive(Clone, Debug, Eq, PartialEq, Snafu)]
49pub enum TemplateParseError {
50    #[snafu(display("Invalid strftime item"))]
51    StrftimeError,
52    #[snafu(display(
53        "Invalid field path in template {:?} (see https://vector.dev/docs/reference/configuration/template-syntax/)",
54        path
55    ))]
56    InvalidPathSyntax { path: String },
57    #[snafu(display("Invalid numeric template"))]
58    InvalidNumericTemplate { template: String },
59}
60
61/// Errors raised whilst rendering a Template.
62#[allow(missing_docs)]
63#[derive(Clone, Debug, Eq, PartialEq, Snafu)]
64pub enum TemplateRenderingError {
65    #[snafu(display("Missing fields on event: {:?}", missing_keys))]
66    MissingKeys { missing_keys: Vec<String> },
67    #[snafu(display("Not numeric: {:?}", input))]
68    NotNumeric { input: String },
69    /// The rendered value was rejected by the confinement check attached to
70    /// this template — the event should be dropped as an intentional discard.
71    ///
72    /// `rendered_preview` is bounded to [`CONFINED_PREVIEW_BYTES`] bytes to
73    /// avoid two problems: leaking secrets in fields that carry credentials
74    /// (e.g. `Authorization: Bearer ...` header templates), and amplifying
75    /// attacker-controlled oversized field values into logs.
76    #[snafu(display(
77        "rendered value ({rendered_len} bytes, preview {rendered_preview:?}) \
78         confined: {message}"
79    ))]
80    Confined {
81        rendered_preview: String,
82        rendered_len: usize,
83        message: String,
84    },
85}
86
87/// Maximum number of bytes of a rejected rendered value to include in a
88/// [`TemplateRenderingError::Confined`] error. Kept small so log lines
89/// remain bounded even under attacker-controlled input.
90pub const CONFINED_PREVIEW_BYTES: usize = 32;
91
92/// Build a bounded preview of a rendered value for inclusion in
93/// [`TemplateRenderingError::Confined`]. Truncates on a UTF-8 char boundary.
94pub fn confined_preview(rendered: &str) -> String {
95    if rendered.len() <= CONFINED_PREVIEW_BYTES {
96        return rendered.to_string();
97    }
98    let mut end = CONFINED_PREVIEW_BYTES;
99    while end > 0 && !rendered.is_char_boundary(end) {
100        end -= 1;
101    }
102    rendered.get(..end).unwrap_or("").to_string()
103}
104
105/// A templated field.
106///
107/// In many cases, components can be configured so that part of the component's functionality can be
108/// customized on a per-event basis. By using `UnconfinedTemplate`, users can specify either fixed
109/// strings or templated strings. Templated strings use a common syntax to refer to fields in an
110/// event that is used as the input data when rendering the template.
111#[configurable_component]
112#[configurable(metadata(docs::templateable))]
113#[derive(Clone, Default, PartialEq, Eq, Hash)]
114#[serde(try_from = "String", into = "String")]
115pub struct UnconfinedTemplate {
116    src: String,
117
118    #[serde(skip)]
119    parts: Vec<Part>,
120
121    #[serde(skip)]
122    is_static: bool,
123
124    #[serde(skip)]
125    reserve_size: usize,
126
127    #[serde(skip)]
128    tz_offset: Option<FixedOffset>,
129}
130
131/// A template that has passed through confinement via [`Template::confine`].
132///
133/// This is the only render-capable, confinement-enforcing template type. It is deliberately **not**
134/// deserializable: the sole way to obtain one is by confining a [`Template`] or an
135/// [`UnconfinedTemplate`], so a rendered value can never escape its confinement boundary. The
136/// `render` methods enforce the attached confinement checker (if any).
137///
138/// Both fields are private to this module, so a `ConfinedTemplate` can
139/// never be constructed (or deserialized) without going through confinement.
140#[derive(Clone, PartialEq, Eq, Hash)]
141pub struct ConfinedTemplate {
142    inner: UnconfinedTemplate,
143    checker: Option<ConfinementChecker>,
144}
145
146/// The templated field type stored in sink config structs.
147///
148/// `Template` is serde-able and appears as a plain string in generated configuration schemas, but
149/// it exposes **no** `render` method. To render it a sink must first call [`Template::confine`] in
150/// its `build()`, which yields a [`ConfinedTemplate`] that enforces the confinement invariant at
151/// render time. This makes confinement unavoidable: there is no way to render a sink's configured
152/// template without going through confinement first.
153///
154/// Transforms and sources, which have no confinement boundary, should store
155/// [`UnconfinedTemplate`] directly instead.
156#[configurable_component]
157#[configurable(metadata(docs::templateable))]
158#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
159#[serde(try_from = "String", into = "String")]
160pub struct Template {
161    /// Inner template
162    #[serde(skip)]
163    inner: UnconfinedTemplate,
164}
165
166/// Unsigned integer template.
167#[configurable_component]
168#[configurable(metadata(docs::templateable))]
169#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
170#[serde(
171    try_from = "UnsignedIntTemplateSource",
172    into = "UnsignedIntTemplateSource"
173)]
174pub struct UnsignedIntTemplate {
175    src: UnsignedIntTemplateSource,
176
177    #[serde(skip)]
178    parts: Vec<Part>,
179
180    #[serde(skip)]
181    tz_offset: Option<FixedOffset>,
182}
183
184/// Serializable config fragment for template confinement.
185///
186/// Embed this in a component config with `#[serde(flatten)]` to get the
187/// `dangerously_allow_unconfined_template_resolution` field. Pass it to
188/// [`Template::confine`] on each template the component owns.
189#[configurable_component]
190#[derive(Clone, Debug, Default)]
191pub struct ConfinementConfig {
192    /// Disable all template confinement checks for this sink.
193    ///
194    /// **DANGEROUS — disables a security control.**
195    ///
196    /// Bypasses both startup validation and runtime confinement for every
197    /// templated field on this sink. When enabled, a log producer that
198    /// controls any field used in a template can write to arbitrary keys,
199    /// paths, or routing destinations. This flag is a full opt-out: it
200    /// disables confinement even for templates that have a usable static
201    /// prefix.
202    #[serde(default)]
203    pub dangerously_allow_unconfined_template_resolution: bool,
204}
205
206#[cfg(test)]
207mod tests;