Information disclosure / improper tenant isolation

MEDIUM
victoriametrics/victoriametrics
Commit: 0dd2b2cee685
Affected: < 1.139.0
2026-06-24 14:39 UTC

Description

Summary: The commit fixes incorrect tenant filtering when long tenant regular expressions are used, which could lead to information disclosure across tenants due to improper tenant isolation. The root cause was that tenant filters were converted to a human-readable string via TagFilter.String(), and for long regexes this representation could be truncated to an ellipsis (...). Such truncated filter representations could be interpreted incorrectly during evaluation, causing tenants to be matched inaccurately and potentially exposing data to unintended tenants. The fix stops relying on the truncated, human-readable form and builds the filter terms using explicit operator handling (equality, inequality, regex, and negative variants) so the full semantics of the filter are preserved regardless of length. It also extends parsing logic (ParseFromMetricExpr) to better preserve filter semantics when converting from metric expressions. Impact: If unpatched, a long tenant regex could cause incorrect tenant matching, exposing data to tenants that should not have access. The patch improves tenant isolation and access control by ensuring long filters are interpreted correctly. This aligns with information-disclosure prevention in multi-tenant contexts.

Proof of Concept

Proof-of-concept (conceptual, actionable steps): Context: VictoriaMetrics in a multi-tenant setup with per-tenant data access. Tenants: 100, 108, 116, 1239 (with representative data). A long tenant filter regex is used to select a subset of tenants. Problem (before fix): If the tenant filter value is very long, the internal representation via TagFilter.String() can be truncated to a placeholder like '...'. When the engine later evaluates the filter, the truncated representation can cause incorrect matching semantics. This could result in data from unintended tenants (e.g., tenant 1239) being included in a query intended for a different subset (e.g., 100, 108, 116). Attack scenario (high-level): 1) Deploy VictoriaMetrics with multi-tenant mode and populate sample data for tenants: 100, 108, 116, 1239. 2) Issue a query that uses a long regex-based tenant filter, e.g. value = long_regex_pattern_matching_multiple_tenants (IsRegexp = true). 3) Observe results: due to the old string representation, the filter may be evaluated incorrectly, causing data leakage from one or more tenants not intended by the filter (for example, including 1239 data in a result set that should have excluded it, or omitting 108). Reproduction outline (pseudocode and steps): - Prepare data for tenants: 100, 108, 116, 1239. - Construct a TagFilter for tenant_id with a very long regex, IsRegexp = true, e.g.: Key: "tenant_id" Value: "^(100|101|102|...|ReplicaOfLongRegexWithManyTerms|1239)$" // intentionally long IsRegexp: true - Execute a query that applies this tenant filter and returns a small, verifiable metric (e.g., count or sum). - Observe that, on an unpatched build, the result set may include data from an unintended tenant (e.g., 1239) or miss an intended tenant (e.g., 108). What to test after patch: - Run the same long-regex-based filter and verify that results now include exactly the intended tenants without leakage, e.g., [100, 108, 116] or the exact set dictated by the long regex, but without data from unintended tenants. Notes: The test TestApplyFiltersToTenants in the referenced test suite demonstrates a mismatch caused by long tenant filters. The fix ensures the operator for each filter term is preserved (e.g., =, !=, =~, !~) instead of relying on a truncated human-readable representation, thus preserving correct semantics.

Commit Details

Author: Alexander Frolov

Date: 2026-06-19 07:54 UTC

Message:

app/vmselect: fix tenant filtering with long tenant regexp When tenant filter is a long regexp, its content can be replaced with `...`, causing tenants to be matched incorrectly. `applyFiltersToTenants` converts tag filters using `tagFiltersToString` https://github.com/VictoriaMetrics/VictoriaMetrics/blob/c497c8c2e90962f5d77785d4333287d978cd2d1b/app/vmselect/netstorage/tenant_filters.go#L106-L112 Which uses human-readable representation of `TagFilter` https://github.com/VictoriaMetrics/VictoriaMetrics/blob/c497c8c2e90962f5d77785d4333287d978cd2d1b/lib/storage/search.go#L390-L397 This way I can see results from unexpected tenants. See the test, which fails with ``` --- FAIL: TestApplyFiltersToTenants (0.00s) tenant_filters_test.go:18: unexpected tenants result; got [{100 0} {116 0} {1239 0}]; want [{100 0} {108 0} {116 0}] ``` --------- Related PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11096

Triage Assessment

Vulnerability Type: Information disclosure

Confidence: MEDIUM

Reasoning:

The commit addresses incorrect tenant filtering when long tenant regular expressions are used, which could cause tenants to be matched incorrectly and potentially expose data to unintended tenants. This relates to tenant isolation and information disclosure. The changes replace a simplistic string representation with explicit operator handling to avoid truncation/obscuring of filter terms, improving correct access control.

Verification Assessment

Vulnerability Type: Information disclosure / improper tenant isolation

Confidence: MEDIUM

Affected Versions: < 1.139.0

Code Diff

diff --git a/app/vmctl/vm_native.go b/app/vmctl/vm_native.go index 2ca364a87bc46..de47a2eeb5430 100644 --- a/app/vmctl/vm_native.go +++ b/app/vmctl/vm_native.go @@ -405,7 +405,16 @@ func buildMatchWithFilter(filter string, metricName string) (string, error) { if len(tf.Key) == 0 { continue } - a = append(a, tf.String()) + switch { + case tf.IsNegative && tf.IsRegexp: + a = append(a, fmt.Sprintf("%s!~%q", tf.Key, tf.Value)) + case tf.IsNegative: + a = append(a, fmt.Sprintf("%s!=%q", tf.Key, tf.Value)) + case tf.IsRegexp: + a = append(a, fmt.Sprintf("%s=~%q", tf.Key, tf.Value)) + default: + a = append(a, fmt.Sprintf("%s=%q", tf.Key, tf.Value)) + } } a = append(a, nameFilter) filters = append(filters, strings.Join(a, ",")) diff --git a/lib/promrelabel/if_expression.go b/lib/promrelabel/if_expression.go index 3afd69f5ed218..322ddb4760983 100644 --- a/lib/promrelabel/if_expression.go +++ b/lib/promrelabel/if_expression.go @@ -50,6 +50,17 @@ func (ie *IfExpression) Parse(s string) error { return nil } +// ParseFromMetricExpr parses if from given MetricExpr +func (ie *IfExpression) ParseFromMetricExpr(me *metricsql.MetricExpr) error { + var ieLocal ifExpression + if err := ieLocal.parseFromMetricExpr(me); err != nil { + return err + } + + ie.ies = []*ifExpression{&ieLocal} + return nil +} + // UnmarshalJSON unmarshals ie from JSON data. func (ie *IfExpression) UnmarshalJSON(data []byte) error { var v any @@ -182,6 +193,16 @@ func (ie *ifExpression) Parse(s string) error { return nil } +func (ie *ifExpression) parseFromMetricExpr(me *metricsql.MetricExpr) error { + lfss, err := metricExprToLabelFilterss(me) + if err != nil { + return fmt.Errorf("cannot parse series selector: %w", err) + } + ie.s = string(me.AppendString(nil)) + ie.lfss = lfss + return nil +} + // UnmarshalJSON unmarshals ie from JSON data. func (ie *ifExpression) UnmarshalJSON(data []byte) error { var s string
← Back to Alerts View on GitHub →