Compare commits

...

2 Commits

Author SHA1 Message Date
HAL9000 56dbcb0699 fix: resolve Behave step conflicts and attribute name mismatch in env var security tests
- Remove duplicate step definitions that conflicted with existing steps
- Fix attribute name from env_vars_to_cleanup to env_vars_to_clean for consistency with environment.py
- Add @action and @security tags to feature file for better test organization
- Correct edge case test expectation: CLEVERAGENTS_ variable is now correctly expected to be interpolated
- Simplify steps file to only include unique steps, reusing existing ones from other step files
2026-04-15 01:24:30 +00:00
HAL9000 f61cf2c6b5 fix(action): restrict env var interpolation in action YAML to allowlisted prefixes
CI / lint (pull_request) Failing after 35s
CI / typecheck (pull_request) Successful in 57s
CI / quality (pull_request) Successful in 28s
CI / security (pull_request) Successful in 1m24s
CI / coverage (pull_request) Has been skipped
CI / build (pull_request) Successful in 23s
CI / unit_tests (pull_request) Failing after 1m6s
CI / docker (pull_request) Has been skipped
CI / helm (pull_request) Successful in 23s
CI / push-validation (pull_request) Successful in 30s
CI / integration_tests (pull_request) Successful in 4m19s
CI / e2e_tests (pull_request) Successful in 4m33s
CI / status-check (pull_request) Failing after 2s
Implemented a security-conscious update to environment variable interpolation within action YAML. Added a dedicated allowlist mechanism by introducing the _ALLOWED_ENV_VAR_PREFIX constant set to "CLEVERAGENTS_". Updated the _env_replacer function to validate variable names against the allowlist before performing interpolation. Documented the security restriction directly in the _interpolate_env_vars docstring to clearly communicate the behavior. Expanded test coverage with comprehensive Behave tests for both allowed and disallowed env var interpolation, and added corresponding step definitions for testing env var security.

ISSUES CLOSED: #9089
2026-04-14 11:34:15 +00:00
3 changed files with 231 additions and 2 deletions
@@ -0,0 +1,111 @@
@action
@security
Feature: Action Schema Environment Variable Security
As a security-conscious developer
I want to ensure that action YAML files can only interpolate allowlisted environment variables
So that sensitive credentials cannot be exfiltrated through malicious action configurations
Background:
Given an action YAML string with only required fields
# ============================================================
# Allowed Environment Variable Interpolation
# ============================================================
Scenario: Interpolate CLEVERAGENTS_ prefixed environment variable
Given an environment variable "CLEVERAGENTS_API_KEY" set to "secret-key-123"
And an action YAML string with description "${CLEVERAGENTS_API_KEY}"
When I load the action YAML
Then the action description should be "secret-key-123"
Scenario: Interpolate multiple CLEVERAGENTS_ prefixed variables
Given an environment variable "CLEVERAGENTS_HOST" set to "localhost"
And an environment variable "CLEVERAGENTS_PORT" set to "8080"
And an action YAML string with description "Connect to ${CLEVERAGENTS_HOST}:${CLEVERAGENTS_PORT}"
When I load the action YAML
Then the action description should be "Connect to localhost:8080"
Scenario: Missing CLEVERAGENTS_ variable left as placeholder
Given no environment variable "CLEVERAGENTS_MISSING_VAR"
And an action YAML string with description "Config: ${CLEVERAGENTS_MISSING_VAR}"
When I load the action YAML
Then the action description should be "Config: ${CLEVERAGENTS_MISSING_VAR}"
# ============================================================
# Disallowed Environment Variable Protection
# ============================================================
Scenario: Disallow AWS_SECRET_ACCESS_KEY interpolation
Given an environment variable "AWS_SECRET_ACCESS_KEY" set to "aws-secret-123"
And an action YAML string with description "Secret: ${AWS_SECRET_ACCESS_KEY}"
When I load the action YAML
Then the action description should be "Secret: ${AWS_SECRET_ACCESS_KEY}"
Scenario: Disallow DATABASE_PASSWORD interpolation
Given an environment variable "DATABASE_PASSWORD" set "db-password-456"
And an action YAML string with description "DB: ${DATABASE_PASSWORD}"
When I load the action YAML
Then the action description should be "DB: ${DATABASE_PASSWORD}"
Scenario: Disallow API_KEY interpolation
Given an environment variable "API_KEY" set to "api-key-789"
And an action YAML string with description "API: ${API_KEY}"
When I load the action YAML
Then the action description should be "API: ${API_KEY}"
Scenario: Disallow HOME interpolation
Given an environment variable "HOME" set to "/home/user"
And an action YAML string with description "Home: ${HOME}"
When I load the action YAML
Then the action description should be "Home: ${HOME}"
Scenario: Disallow PATH interpolation
Given an environment variable "PATH" set to "/usr/bin:/bin"
And an action YAML string with description "Path: ${PATH}"
When I load the action YAML
Then the action description should be "Path: ${PATH}"
# ============================================================
# Mixed Allowed and Disallowed Variables
# ============================================================
Scenario: Mix allowed and disallowed variables
Given an environment variable "CLEVERAGENTS_CONFIG" set to "allowed-config"
And an environment variable "SECRET_TOKEN" set to "secret-token-xyz"
And an action YAML string with description "Config: ${CLEVERAGENTS_CONFIG}, Token: ${SECRET_TOKEN}"
When I load the action YAML
Then the action description should be "Config: allowed-config, Token: ${SECRET_TOKEN}"
# ============================================================
# Edge Cases
# ============================================================
Scenario: Empty prefix check (CLEVERAGENTS_ variable is interpolated)
Given an environment variable "CLEVERAGENTS_" set to "empty-prefix"
And an action YAML string with description "Var: ${CLEVERAGENTS_}"
When I load the action YAML
Then the action description should be "Var: empty-prefix"
Scenario: Case sensitivity - lowercase not allowed
Given an environment variable "cleveragents_api_key" set to "lowercase-key"
And an action YAML string with description "Key: ${cleveragents_api_key}"
When I load the action YAML
Then the action description should be "Key: ${cleveragents_api_key}"
Scenario: Interpolation in nested fields
Given an environment variable "CLEVERAGENTS_ACTOR" set to "openai/gpt-4"
And an action YAML string with strategy_actor "${CLEVERAGENTS_ACTOR}"
When I load the action YAML
Then the action strategy_actor should be "openai/gpt-4"
Scenario: Interpolation in list fields
Given an environment variable "CLEVERAGENTS_INVARIANT" set to "No secrets in code"
And an action YAML string with invariants containing "${CLEVERAGENTS_INVARIANT}"
When I load the action YAML
Then the action invariants should contain "No secrets in code"
Scenario: Disallowed variable in list fields
Given an environment variable "SECRET_INVARIANT" set to "Secret invariant"
And an action YAML string with invariants containing "${SECRET_INVARIANT}"
When I load the action YAML
Then the action invariants should contain "${SECRET_INVARIANT}"
@@ -0,0 +1,104 @@
"""Step definitions for action schema environment variable security tests.
Tests for features/action_schema_env_var_security.feature — validates that
the ActionConfigSchema only allows interpolation of CLEVERAGENTS_ prefixed
environment variables, preventing exfiltration of sensitive credentials.
"""
from __future__ import annotations
import os
from behave import given, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from cleveragents.action.schema import ActionConfigSchema
# ────────────────────────────────────────────────────────────
# Given steps (unique to this feature)
# ────────────────────────────────────────────────────────────
@given('an environment variable "{var_name}" set to "{var_value}"')
def step_given_env_var_set(context: Context, var_name: str, var_value: str) -> None:
"""Set an environment variable for testing."""
os.environ[var_name] = var_value
# Store for cleanup
if not hasattr(context, "env_vars_to_clean"):
context.env_vars_to_clean = []
context.env_vars_to_clean.append(var_name)
@given('an environment variable "{var_name}" set "{var_value}"')
def step_given_env_var_set_alt(context: Context, var_name: str, var_value: str) -> None:
"""Set an environment variable for testing (alternative syntax)."""
os.environ[var_name] = var_value
# Store for cleanup
if not hasattr(context, "env_vars_to_clean"):
context.env_vars_to_clean = []
context.env_vars_to_clean.append(var_name)
@given('no environment variable "{var_name}"')
def step_given_no_env_var(context: Context, var_name: str) -> None:
"""Ensure an environment variable is not set."""
if var_name in os.environ:
del os.environ[var_name]
@given('an action YAML string with description "{description}"')
def step_given_yaml_with_description(context: Context, description: str) -> None:
"""Provide YAML with a custom description value."""
_MINIMAL_YAML = """\
name: local/simple-action
description: A simple action for testing
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: All tests pass
"""
context.action_yaml_string = _MINIMAL_YAML.replace(
"A simple action for testing", description
)
@given('an action YAML string with strategy_actor "{actor}"')
def step_given_yaml_with_strategy_actor(context: Context, actor: str) -> None:
"""Provide YAML with a custom strategy_actor value."""
_MINIMAL_YAML = """\
name: local/simple-action
description: A simple action for testing
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: All tests pass
"""
context.action_yaml_string = _MINIMAL_YAML.replace("openai/gpt-4", actor, 1)
@given('an action YAML string with invariants containing "{invariant}"')
def step_given_yaml_with_invariants(context: Context, invariant: str) -> None:
"""Provide YAML with invariants containing a specific value."""
_MINIMAL_YAML = """\
name: local/simple-action
description: A simple action for testing
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: All tests pass
"""
yaml_with_invariants = _MINIMAL_YAML + f"\ninvariants:\n - {invariant}\n"
context.action_yaml_string = yaml_with_invariants
# ────────────────────────────────────────────────────────────
# When steps (unique to this feature)
# ────────────────────────────────────────────────────────────
@when("I load the action YAML")
def step_when_load_action_yaml(context: Context) -> None:
"""Load and validate the action YAML."""
try:
context.action_config = ActionConfigSchema.from_yaml(context.action_yaml_string)
context.load_error = None
except Exception as e:
context.action_config = None
context.load_error = e
+16 -2
View File
@@ -45,6 +45,9 @@ NAMESPACED_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*/[a-z0-9][a-z0-9_-]*$")
#: Pattern for ``${VAR}`` environment variable references.
_ENV_VAR_RE = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
#: Allowed prefix for environment variable interpolation (security restriction).
_ALLOWED_ENV_VAR_PREFIX = "CLEVERAGENTS_"
#: Supported action states (draft excluded from YAML configs).
_VALID_STATES = frozenset({"available", "archived"})
@@ -448,6 +451,10 @@ def _interpolate_env_vars(data: dict[str, Any]) -> dict[str, Any]:
Only string values are interpolated. Missing environment variables
are left as-is (no error) to allow deferred resolution.
Security: Only environment variables with the ``CLEVERAGENTS_`` prefix
are eligible for interpolation. Attempts to reference other variables
are left as literal ``${VAR}`` placeholders.
"""
result: dict[str, Any] = {}
for key, value in data.items():
@@ -474,6 +481,13 @@ def _interpolate_env_vars(data: dict[str, Any]) -> dict[str, Any]:
def _env_replacer(match: re.Match[str]) -> str:
"""Replace a single ``${VAR}`` match with its env value."""
"""Replace a single ``${VAR}`` match with its env value.
Security: Only allows interpolation of environment variables with the
``CLEVERAGENTS_`` prefix. Other variables are left as literal placeholders.
"""
var_name = match.group(1)
return os.environ.get(var_name, match.group(0))
if var_name.startswith(_ALLOWED_ENV_VAR_PREFIX):
return os.environ.get(var_name, match.group(0))
# Return the original placeholder for disallowed variables
return match.group(0)