Reliance on Untrusted Inputs in a Security Decision

Description

Reliance on Untrusted Inputs in a Security Decision occurs when software uses inputs that can be modified by untrusted actors to make security-critical decisions about authentication, authorization, or access control. Developers often mistakenly assume that certain inputs cannot be altered—cookies, environment variables, hidden form fields, HTTP headers, or URL parameters—but attackers using customized clients, intercepting proxies, or direct API calls can modify any client-provided data. When security decisions depend on these unvalidated values, attackers can bypass access controls, escalate privileges, or impersonate other users.

Risk

This vulnerability enables direct bypass of security controls. Attackers can modify cookies to assume different user identities, alter hidden fields to change pricing or permissions, manipulate HTTP headers to bypass IP-based restrictions, or forge environment variables to gain elevated privileges. The impact ranges from unauthorized access to sensitive data, through privilege escalation, to complete system compromise. The vulnerability is particularly insidious because the application appears to implement security checks—they simply rely on inputs the attacker controls. Trust in client-provided data is a fundamental security anti-pattern.

Solution

Never trust client-provided inputs for security decisions. Store authoritative security state server-side only, using secure session management. When client-side storage is necessary, use cryptographic integrity protection (HMAC, digital signatures) and encryption. Validate all inputs against server-side authoritative state. Implement defense-in-depth by duplicating client-side security checks on the server. Use vetted frameworks that manage authentication state automatically. Identify all inputs that could influence security decisions and trace their origins. For DNS-based decisions, implement additional verification mechanisms. In PHP, disable register_globals and avoid extracting untrusted data into the symbol table.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Attackers can modify inputs to bypass security checks entirely.
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Manipulated inputs can grant unauthorized access or allow impersonation of other users.
VariesScope: Varies by Context

Other - Consequences depend on what the security decision protects: data exposure, code execution, financial loss, etc.

Example Code

Vulnerable Code

// Vulnerable: Reading user role from cookie
public class VulnerableRoleCheck {

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        String role = "user";  // Default role

        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("role")) {
                // Vulnerable: Trust cookie value for authorization
                role = cookie.getValue();
            }
        }

        if (role.equals("admin")) {
            // Attacker can set cookie "role=admin" to gain access
            showAdminPanel(response);
        } else {
            showUserPanel(response);
        }
    }
}
// Vulnerable: Authentication via cookie
<?php
function vulnerable_auth_check() {
    // Vulnerable: Trusts cookie for authentication
    $authenticated = false;

    if (isset($_COOKIE['authenticated'])) {
        // Attacker sets cookie "authenticated=true"
        $authenticated = ($_COOKIE['authenticated'] === 'true');
    }

    if ($authenticated) {
        show_protected_content();
    } else {
        show_login_form();
    }
}
?>
// Vulnerable: Authentication state in client cookie
public class VulnerableAuthCookie {

    public boolean isAuthenticated(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();

        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("authenticated")) {
                // Vulnerable: Boolean stored in client cookie
                return Boolean.parseBoolean(cookie.getValue());
            }
        }
        return false;
    }
}
# Vulnerable: Trust hidden form field
from flask import Flask, request

app = Flask(__name__)

@app.route('/purchase', methods=['POST'])
def vulnerable_purchase():
    # Vulnerable: Price from hidden form field
    price = float(request.form.get('price', 0))
    item_id = request.form.get('item_id')

    # Attacker modifies hidden field to pay $0.01
    process_payment(price)
    deliver_item(item_id)

    return "Purchase complete"
// Vulnerable: DNS-based trust decision
#include <netdb.h>

int vulnerable_dns_trust(int socket) {
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);
    struct hostent *hp;

    getpeername(socket, (struct sockaddr *)&addr, &addrlen);

    // Vulnerable: Reverse DNS can be spoofed
    hp = gethostbyaddr(&addr.sin_addr, sizeof(addr.sin_addr), AF_INET);

    if (hp != NULL && strstr(hp->h_name, ".trusted.com")) {
        // Attacker with DNS cache poisoning gains access
        return GRANT_ACCESS;
    }
    return DENY_ACCESS;
}
// Vulnerable: Trust user ID from localStorage
async function vulnerableFetchProfile() {
    // Vulnerable: User ID from client storage
    const userId = localStorage.getItem('userId');

    // Attacker modifies localStorage to access other profiles
    const response = await fetch(`/api/users/${userId}/profile`);
    return response.json();
}
// Vulnerable: register_globals behavior
<?php
// With register_globals=On (deprecated but instructive)
// URL: page.php?admin=true

function vulnerable_admin_check() {
    global $admin;  // Comes from URL parameter!

    if ($admin == true) {
        // Attacker adds ?admin=true to URL
        perform_admin_action();
    }
}
?>

Fixed Code

// Fixed: Server-side role management
public class FixedRoleCheck {

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        // Fixed: Get role from server-side session
        HttpSession session = request.getSession(false);

        if (session == null) {
            redirectToLogin(response);
            return;
        }

        // Role stored server-side, not in cookie
        String role = (String) session.getAttribute("role");

        if ("admin".equals(role)) {
            showAdminPanel(response);
        } else {
            showUserPanel(response);
        }
    }
}
// Fixed: Server-side session authentication
<?php
session_start();

function fixed_auth_check() {
    // Fixed: Check server-side session, not cookie
    if (isset($_SESSION['authenticated']) &&
        $_SESSION['authenticated'] === true &&
        isset($_SESSION['user_id'])) {

        // Verify session hasn't expired
        if (time() - $_SESSION['last_activity'] < SESSION_TIMEOUT) {
            $_SESSION['last_activity'] = time();
            show_protected_content();
            return;
        }
    }

    // Not authenticated or session expired
    session_destroy();
    show_login_form();
}
?>
// Fixed: Signed/encrypted authentication token
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class FixedAuthToken {
    private static final String SECRET_KEY = System.getenv("AUTH_SECRET_KEY");

    public String createToken(String userId, String role, long expiry) {
        String data = userId + ":" + role + ":" + expiry;
        String signature = sign(data);
        return Base64.getEncoder().encodeToString(
            (data + ":" + signature).getBytes()
        );
    }

    public AuthData verifyToken(String token) {
        try {
            String decoded = new String(Base64.getDecoder().decode(token));
            String[] parts = decoded.split(":");
            if (parts.length != 4) return null;

            String data = parts[0] + ":" + parts[1] + ":" + parts[2];
            String signature = parts[3];

            // Fixed: Verify cryptographic signature
            if (!sign(data).equals(signature)) {
                return null;  // Tampered token
            }

            // Check expiry
            long expiry = Long.parseLong(parts[2]);
            if (System.currentTimeMillis() > expiry) {
                return null;  // Expired
            }

            return new AuthData(parts[0], parts[1]);
        } catch (Exception e) {
            return null;
        }
    }

    private String sign(String data) {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(SECRET_KEY.getBytes(), "HmacSHA256"));
        return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes()));
    }
}
# Fixed: Server-side price lookup
from flask import Flask, request, session

app = Flask(__name__)

@app.route('/purchase', methods=['POST'])
def fixed_purchase():
    item_id = request.form.get('item_id')

    # Fixed: Get price from server database, not client
    item = get_item_from_database(item_id)
    if not item:
        return "Item not found", 404

    # Use server-authoritative price
    price = item['price']

    # Verify user has permission to purchase
    if not session.get('user_id'):
        return "Not authenticated", 401

    process_payment(session['user_id'], price)
    deliver_item(session['user_id'], item_id)

    return "Purchase complete"
// Fixed: Additional verification beyond DNS
#include <netdb.h>
#include <openssl/ssl.h>

int fixed_verify_peer(SSL *ssl) {
    X509 *cert;
    X509_NAME *subject;
    char common_name[256];

    // Fixed: Verify client certificate
    cert = SSL_get_peer_certificate(ssl);
    if (cert == NULL) {
        return DENY_ACCESS;
    }

    // Verify certificate is valid
    if (SSL_get_verify_result(ssl) != X509_V_OK) {
        X509_free(cert);
        return DENY_ACCESS;
    }

    // Check certificate CN against allowlist
    subject = X509_get_subject_name(cert);
    X509_NAME_get_text_by_NID(subject, NID_commonName,
                               common_name, sizeof(common_name));

    int result = is_in_allowlist(common_name) ? GRANT_ACCESS : DENY_ACCESS;

    X509_free(cert);
    return result;
}
// Fixed: Server validates user access
async function fixedFetchProfile() {
    // Fixed: Server validates session, not client-provided user ID
    const response = await fetch('/api/profile', {
        credentials: 'include'  // Send session cookie
    });

    if (response.status === 401) {
        redirectToLogin();
        return null;
    }

    // Server determines which profile to return based on session
    return response.json();
}

// Server-side (Express)
app.get('/api/profile', authenticateSession, (req, res) => {
    // req.user set by authenticateSession middleware from session
    const profile = getProfile(req.user.id);
    res.json(profile);
});

  • CWE-693: Protection Mechanism Failure (parent)
  • CWE-302: Authentication Bypass by Assumed-Immutable Data (child)
  • CWE-350: Reliance on Reverse DNS Resolution for a Security-Critical Action (child)
  • CWE-784: Reliance on Cookies without Validation and Integrity Checking (child)
  • CWE-602: Client-Side Enforcement of Server-Side Security (related)

References

  1. MITRE Corporation. "CWE-807: Reliance on Untrusted Inputs in a Security Decision." https://cwe.mitre.org/data/definitions/807.html
  2. OWASP. "Insecure Direct Object References." https://owasp.org/www-project-web-security-testing-guide/
  3. OWASP. "Session Management Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html