Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- `opentelemetry-sdk`: fix YAML structure injection via environment variable substitution in declarative file configuration; values containing newlines are now emitted as quoted YAML scalars per spec requirement
([#5091](https://github.com/open-telemetry/opentelemetry-python/pull/5091))
- `opentelemetry-sdk`: Add `create_logger_provider`/`configure_logger_provider` to declarative file configuration, enabling LoggerProvider instantiation from config files without reading env vars
([#4990](https://github.com/open-telemetry/opentelemetry-python/pull/4990))
- `opentelemetry-sdk`: Add `service` resource detector support to declarative file configuration via `detection_development.detectors[].service`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ def replace_var(match) -> str:
f"Environment variable '{var_name}' not found and no default provided"
)

# Per spec: "It MUST NOT be possible to inject YAML structures by
# environment variables." Newlines are the primary injection vector —
# a value like "legit\nmalicious_key: val" would create extra YAML
# keys if substituted verbatim. Wrap such values in a YAML
# double-quoted scalar so the newline is treated as literal text.
# Simple values (no newlines) are returned as-is so that YAML type
# coercion still applies per spec ("Node types MUST be interpreted
# after environment variable substitution takes place").
if "\n" in value or "\r" in value:
escaped = (
value.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
)
return f'"{escaped}"'
return value

return re.sub(pattern, replace_var, text)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import unittest
from unittest.mock import patch

import yaml

from opentelemetry.sdk._configuration.file import (
EnvSubstitutionError,
substitute_env_vars,
Expand Down Expand Up @@ -115,3 +117,45 @@ def test_only_dollar_signs(self):
"""Test string with only escaped dollar signs."""
result = substitute_env_vars("$$$$")
self.assertEqual(result, "$$")

def test_newline_in_value_prevents_yaml_injection(self):
"""Values containing newlines must not inject YAML structure.
Per spec: "It MUST NOT be possible to inject YAML structures by
environment variables." A value like "legit\\nmalicious_key: val"
must be emitted as a quoted scalar, not raw YAML.
"""
with patch.dict(
os.environ,
{"SERVICE_NAME": "legit-service\nmalicious_key: injected_value"},
):
result = substitute_env_vars(
"file_format: '0.1'\nservice_name: ${SERVICE_NAME}"
)
parsed = yaml.safe_load(result)
self.assertNotIn("malicious_key", parsed)
self.assertIn("legit-service", parsed["service_name"])

def test_newline_in_value_preserved_as_literal(self):
"""Newline within a value is preserved as a literal newline character."""
with patch.dict(os.environ, {"MULTI": "line1\nline2"}):
result = substitute_env_vars("key: ${MULTI}")
parsed = yaml.safe_load(result)
self.assertEqual(parsed["key"], "line1\nline2")

def test_carriage_return_in_value_is_escaped(self):
"""Carriage return in value is escaped, not injected."""
with patch.dict(os.environ, {"VAL": "text\r\nmore"}):
result = substitute_env_vars("key: ${VAL}")
parsed = yaml.safe_load(result)
self.assertIsInstance(parsed["key"], str)

def test_type_coercion_preserved_for_simple_values(self):
"""Simple values without newlines still undergo YAML type coercion per spec."""
with patch.dict(os.environ, {"BOOL_VAL": "true", "INT_VAL": "42"}):
bool_result = yaml.safe_load(
substitute_env_vars("key: ${BOOL_VAL}")
)
int_result = yaml.safe_load(substitute_env_vars("key: ${INT_VAL}"))
self.assertIs(bool_result["key"], True)
self.assertEqual(int_result["key"], 42)
Loading