Skip to content

CWE-15: External Control of System or Configuration Setting

Overview

This vulnerability occurs when user input is used to control system or application configuration settings, potentially allowing attackers to alter application behavior, security controls, or environment variables.

OWASP Classification

A02:2025 - Security Misconfiguration

Risk

High: If external input can influence system or security-sensitive configuration, attackers may weaken or disable security controls, alter trust boundaries, or redirect application behavior. This often acts as a bypass primitive that enables other vulnerabilities.

Remediation Steps

Core Principle: Never allow untrusted input to directly control system or security-sensitive configuration; all configuration state must be defined, constrained, and enforced by trusted code and deployment mechanisms.

Locate the Externally-Controlled Configuration Setting

Identify where untrusted data flows into configuration settings:

Review scan results:

  • Check scan results: Review the specific file path, line number, and variable where user input controls configuration
  • Trace data flow: Follow the path from source (HTTP parameters, query strings, POST data, headers, cookies, uploaded files) to sink (config object assignment, environment variable setting, system property modification)

Find vulnerabilities proactively (good developer habits):

  • Code review checklist: During PR reviews, flag any code that assigns user input to configuration objects, environment variables, or system properties
  • Search your codebase: Use grep/search for common patterns:
    • config[.*request - Config assignment from requests
    • setProperty.*request - System property from request
    • process.env.*=.*req - Environment variable from request
    • Environment.SetEnvironmentVariable - C# env manipulation
    • Configuration file loading from user-controlled paths
  • Review configuration endpoints: Audit any endpoints with "config", "settings", "admin", "setup" in the URL path
  • Use IDE security plugins: Install plugins like SonarLint, Snyk, or Semgrep for real-time vulnerability detection
  • Run local static analysis: Use tools like Bandit (Python), FindSecBugs (Java), ESLint security plugins (JavaScript) before committing
  • Architectural review: Question any design where users can modify application behavior - is it necessary? Can it be restricted?

Identify the configuration sink:

  • Locate where values are used to modify settings (logging configuration, file paths in config, database connection strings, feature flags, security settings, timeout values)
  • Find vulnerable patterns: config[user_input], environment variable assignment from requests, dynamic configuration loading
  • Assess attack impact: Determine what attackers could achieve (disable security logging, redirect data to attacker-controlled servers, enable debug mode, escalate privileges, cause denial of service)

Validate and Sanitize All Configuration Inputs

Apply strict validation to any configuration values from untrusted sources:

  • Use allowlists: Only accept configuration values from a predefined list of known-good values
  • Validate type and format: Ensure configuration values match expected data type and format
  • Reject invalid values: Don't apply configuration changes if validation fails
  • Range checking: For numeric settings, enforce minimum and maximum values
  • Path validation: For file paths, validate against allowed directories and prevent traversal

Use Secure Defaults and Least Privilege

Rule of thumb: Any configuration change that affects runtime behavior without requiring an application restart should be treated as security-sensitive.

Apply defense-in-depth principles:

  • Set secure defaults: All configuration options should have secure default values
  • Limit scope: Restrict what configuration changes can affect
  • Principle of least privilege: Configuration changes should require elevated permissions
  • Fail securely: If configuration change fails validation, revert to secure default
  • Don't expose sensitive settings: Never allow external control of security-critical settings

Restrict Configuration Interfaces

Control who can modify configuration:

  • Require authentication: Configuration changes should require user authentication
  • Implement authorization: Use role-based access control - only admins can change config
  • Audit all changes: Log all configuration modifications with user context and timestamp
  • Monitor for anomalies: Alert on unexpected or suspicious configuration changes
  • Separate environments: Use different configurations for dev/staging/production

Separate Code and Configuration

Follow configuration management best practices:

  • External configuration files: Store configuration outside of codebase (environment variables, config files)
  • Protect configuration files: Set proper file permissions, make config files read-only for application
  • Version control: Track configuration changes in version control (but not secrets)
  • Environment-specific configs: Use different configurations for different environments
  • Secret management: Use secret managers (AWS Secrets Manager, HashiCorp Vault) for sensitive configs

Test Configuration Manipulation Attempts

Verify the fix prevents malicious configuration changes:

  • Test with invalid values: Try setting configuration to unexpected values (should be rejected)
  • Test with malicious paths: For file path configs, try path traversal ../../etc/passwd
  • Test with URL injection: For URL configs, try javascript: or file:// schemes
  • Test without authorization: Attempt config changes as unauthorized user (should fail)
  • Verify logging: Ensure configuration change attempts are logged

Common Vulnerable Patterns

Direct assignment from user input:

import logging

@app.route('/set_log_level')
def set_log_level():
    # User can set any log level, including DEBUG
    level = request.args.get('level', 'INFO')
    logging.getLogger().setLevel(level)  # Attacker sets 'DEBUG' to expose secrets
    return "Log level updated"
@PostMapping("/config/upload-path")
public Response setUploadPath(@RequestParam String path) {
    // User controls file system path
    config.setProperty("upload.path", path);  // Attacker: "../../etc/passwd"
    return Response.ok("Path updated");
}
app.post('/config/api-endpoint', (req, res) => {
    // User controls API endpoint URL
    const endpoint = req.body.endpoint;
    config.set('api.endpoint', endpoint);  // Attacker: "http://evil.com/steal"
    res.send('Endpoint updated');
});
  • config[key] = request.params[key] - No validation
  • System.setProperty(userKey, userValue) - Unrestricted access
  • process.env[userVar] = userValue - Environment pollution
  • Allowing users to set any config key, not just specific allowed ones
  • Using user input as dictionary/map keys for configuration objects

Insufficient validation:

  • Only checking if value is non-empty, not validating against allowlist
  • Accepting numeric values without range checking
  • Accepting paths without canonicalization or directory restriction
  • Accepting URLs without protocol/domain validation

Missing authorization:

  • Configuration endpoints accessible without authentication
  • No role-based access control for sensitive settings
  • Allowing any authenticated user to modify config (should be admin-only)

Dangerous configuration targets:

  • Log levels (enabling DEBUG exposes sensitive data)
  • File paths (directory traversal, arbitrary file access)
  • URLs/endpoints (redirect to attacker servers)
  • Database connection strings (data exfiltration)
  • Feature flags (enabling dangerous functionality)
  • Security settings (disabling CSRF protection, auth requirements)
  • Timeout values (denial of service via extreme values)

Secure Patterns

Validate log level against allowlist with admin authorization

import logging

ALLOWED_LOG_LEVELS = {'INFO', 'WARNING', 'ERROR', 'CRITICAL'}

@app.route('/set_log_level')
@require_admin_role
def set_log_level():
    level = request.args.get('level', 'INFO').upper()

    if level not in ALLOWED_LOG_LEVELS:
        return "Invalid log level", 400

    logging.getLogger().setLevel(getattr(logging, level))
    audit_log(f"Log level changed to {level} by {current_user}")
    return "Log level updated"

Why this works: Validates log level against an allowlist of safe values, preventing DEBUG mode which could expose sensitive data. Requires admin authorization to change configuration. Audit logs all configuration changes for security monitoring.

Validate and normalize file paths against base directory

@PostMapping("/config/upload-path")
@PreAuthorize("hasRole('ADMIN')")
public Response setUploadPath(@RequestParam String path) {
    // Validate against allowed directories
    Path normalizedPath = Paths.get(path).normalize();
    Path baseDir = Paths.get("/var/app/uploads");

    if (!normalizedPath.startsWith(baseDir)) {
        throw new SecurityException("Path must be within allowed directory");
    }

    if (!Files.isDirectory(normalizedPath)) {
        throw new IllegalArgumentException("Path must be a directory");
    }

    config.setProperty("upload.path", normalizedPath.toString());
    auditLog.info("Upload path changed to {} by {}", path, currentUser);
    return Response.ok("Path updated");
}

Why this works: Normalizes path to resolve .. and . components, preventing directory traversal. Validates path is within allowed base directory using startsWith() check. Verifies path exists and is a directory. Requires admin role for changes.

Validate URLs against protocol and domain allowlist

const ALLOWED_DOMAINS = ['api.example.com', 'api-staging.example.com'];

app.post('/config/api-endpoint', requireAdmin, (req, res) => {
    const endpoint = req.body.endpoint;

    try {
        const url = new URL(endpoint);

        // Only allow HTTPS
        if (url.protocol !== 'https:') {
            return res.status(400).send('Only HTTPS endpoints allowed');
        }

        // Validate domain against allowlist
        if (!ALLOWED_DOMAINS.includes(url.hostname)) {
            return res.status(400).send('Domain not in allowlist');
        }

        config.set('api.endpoint', endpoint);
        auditLog.info(`API endpoint changed to ${endpoint} by ${req.user.id}`);
        res.send('Endpoint updated');
    } catch (error) {
        res.status(400).send('Invalid URL format');
    }
});

Why this works: Parses and validates URL format using URL constructor which throws on invalid URLs. Enforces HTTPS protocol to prevent unencrypted connections. Validates hostname against allowlist of trusted domains. Requires admin authentication.

Validate database host against allowlist

<?php
$ALLOWED_DB_HOSTS = ['localhost', 'db.internal.company.com'];

if (isset($_POST['db_host'])) {
    // Require admin privileges
    if (!$current_user->hasRole('ADMIN')) {
        http_response_code(403);
        die('Unauthorized');
    }

    $db_host = filter_var($_POST['db_host'], FILTER_SANITIZE_STRING);

    // Validate against allowlist
    if (!in_array($db_host, $ALLOWED_DB_HOSTS)) {
        http_response_code(400);
        die('Invalid database host');
    }

    $config['db_host'] = $db_host;
    error_log("DB host changed to $db_host by user " . $current_user->id);
}

Why this works: Validates database host against strict allowlist of known-good servers, preventing connections to attacker-controlled databases. Requires admin role. Sanitizes input and logs changes for audit trail.

Validate environment variables with type checking

private static readonly HashSet<string> ALLOWED_CONFIG_KEYS = new HashSet<string>
{
    "FEATURE_FLAG_A",
    "CACHE_TTL",
    "MAX_UPLOAD_SIZE"
};

[HttpPost("config/env")]
[Authorize(Roles = "Admin")]
public IActionResult SetEnvironment([FromBody] ConfigRequest req)
{
    if (!ALLOWED_CONFIG_KEYS.Contains(req.Key))
    {
        return BadRequest("Configuration key not allowed");
    }

    // Validate value based on key
    if (req.Key == "CACHE_TTL" && !int.TryParse(req.Value, out int ttl))
    {
        return BadRequest("CACHE_TTL must be an integer");
    }

    if (req.Key == "FEATURE_FLAG_A" && !bool.TryParse(req.Value, out _))
    {
        return BadRequest("FEATURE_FLAG_A must be true or false");
    }

    Environment.SetEnvironmentVariable(req.Key, req.Value);
    _auditLogger.Log($"Config {req.Key} set to {req.Value} by {User.Identity.Name}");
    return Ok("Environment updated");
}

Why this works: Validates configuration key against allowlist of permitted keys, preventing arbitrary environment variable manipulation. Performs type checking for each configuration value based on expected type. Requires admin authorization and logs all changes.

Enforce range limits on numeric configuration values

@app.route('/config/timeout')
@require_admin_role
def set_timeout():
    try:
        timeout = int(request.args.get('timeout', 30))
    except ValueError:
        return "Invalid timeout value", 400

    # Enforce reasonable range
    MIN_TIMEOUT = 5
    MAX_TIMEOUT = 300

    if timeout < MIN_TIMEOUT or timeout > MAX_TIMEOUT:
        return f"Timeout must be between {MIN_TIMEOUT} and {MAX_TIMEOUT} seconds", 400

    config['request_timeout'] = timeout
    audit_log(f"Request timeout changed to {timeout}s by {current_user}")
    return "Timeout updated"

Why this works: Validates timeout is a valid integer (catches ValueError on non-numeric input). Enforces minimum and maximum bounds to prevent DoS via extreme values (too low causes failures, too high causes resource exhaustion). Requires admin privileges and logs changes.

Additional Resources