Modification of Assumed-Immutable Data (MAID)

Description

Modification of Assumed-Immutable Data is a vulnerability where the product does not properly protect an assumed-immutable element from being modified by an attacker. This occurs when critical inputs that the program assumes will remain constant can actually be changed. Common vulnerable resources include hidden form fields in web applications, cookies, environment variables, and data retrieved from reverse DNS lookups. Attackers can modify these values to bypass security controls or alter application behavior.

Risk

MAID vulnerabilities allow attackers to manipulate data the application trusts. Hidden form fields containing prices or user roles can be modified to obtain unauthorized discounts or privileges. Cookies storing authentication state can be tampered with to impersonate other users. Environment variables trusted for configuration can be manipulated to change application behavior. The risk is particularly severe when the assumed-immutable data controls security decisions like authentication, authorization, or pricing.

Solution

Implement integrity checks during storage or transmission through untrusted sources. Store sensitive data in trusted locations protected from external influence (server-side sessions instead of client-side cookies). Use cryptographic signatures (HMAC) to detect tampering with data that must traverse untrusted channels. Never trust client-side validation alone - always validate on the server. Avoid using environment variables or external data for security-critical decisions without verification. Treat all data from external sources as potentially modified.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Modify Application Data - Common data types that are attacked are environment variables, web application parameters, and HTTP headers.
IntegrityScope: Integrity

Unexpected State - Attacker can alter application state through modification of data the application assumes is constant.

Example Code

Vulnerable Code

<!-- Vulnerable: Hidden form fields that can be modified -->
<form action="/checkout" method="POST">
    <!-- Vulnerable: Price can be modified by user -->
    <input type="hidden" name="product_id" value="123">
    <input type="hidden" name="price" value="99.99">
    <input type="hidden" name="discount" value="0">

    <!-- Vulnerable: User role in form -->
    <input type="hidden" name="user_role" value="customer">

    <input type="submit" value="Purchase">
</form>

<!-- Attacker modifies DOM or intercepts request:
     price=0.01, discount=99, user_role=admin -->
# Vulnerable: Trusting client-provided data
from flask import Flask, request, session

app = Flask(__name__)

@app.route('/checkout', methods=['POST'])
def vulnerable_checkout():
    # Vulnerable: Price from form (can be modified)
    price = float(request.form.get('price'))
    quantity = int(request.form.get('quantity'))

    total = price * quantity

    # Attacker submitted price=0.01
    process_payment(total)

@app.route('/admin/action', methods=['POST'])
def vulnerable_admin_action():
    # Vulnerable: Role from cookie (can be modified)
    user_role = request.cookies.get('role', 'guest')

    # Attacker sets cookie: role=admin
    if user_role == 'admin':
        perform_admin_action()  # Unauthorized access

@app.route('/api/data')
def vulnerable_api():
    # Vulnerable: Trusting client IP from header (can be spoofed)
    client_ip = request.headers.get('X-Forwarded-For',
                                     request.remote_addr)

    if is_internal_ip(client_ip):
        # Attacker adds header: X-Forwarded-For: 10.0.0.1
        return get_sensitive_data()
// Vulnerable: Trusting mutable array returned by method
public class VulnerablePermissions {

    private String[] adminUsers = {"admin", "root", "superuser"};

    // Vulnerable: Returns reference to internal array
    public String[] getAdminUsers() {
        return adminUsers;  // Caller can modify!
    }

    public boolean isAdmin(String username) {
        for (String admin : adminUsers) {
            if (admin.equals(username)) {
                return true;
            }
        }
        return false;
    }
}

// Attack:
// String[] admins = permissions.getAdminUsers();
// admins[0] = "attacker";  // Now "attacker" is admin!
<?php
// Vulnerable: PHP register_globals style vulnerability
function vulnerable_authenticate() {
    // Vulnerable: $authenticated might come from user input
    // if register_globals is enabled or manual extraction happens

    if (isset($_GET['authenticated'])) {
        $authenticated = $_GET['authenticated'];  // Attacker controls!
    }

    if ($authenticated) {
        // Attacker passes ?authenticated=1
        grant_access();
    }
}

// Vulnerable: Trusting PHP_SELF
function vulnerable_form() {
    // Vulnerable: PHP_SELF can be manipulated
    echo '<form action="' . $_SERVER['PHP_SELF'] . '">';

    // Attacker visits: /page.php/"><script>alert(1)</script>
    // Results in XSS via "immutable" server variable
}
?>

Fixed Code

# Fixed: Server-side data validation
from flask import Flask, request, session
from functools import wraps
import hmac
import hashlib

app = Flask(__name__)
app.secret_key = 'your-secret-key'

def get_product_price(product_id):
    """Get price from server-side database."""
    return database.query("SELECT price FROM products WHERE id = ?", product_id)

@app.route('/checkout', methods=['POST'])
def secure_checkout():
    product_id = request.form.get('product_id')
    quantity = int(request.form.get('quantity'))

    # Fixed: Get price from server, not client
    price = get_product_price(product_id)

    if price is None:
        return "Invalid product", 400

    total = price * quantity
    process_payment(total)

def login_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        # Fixed: Get role from server-side session, not cookie
        if 'user_id' not in session:
            return "Unauthorized", 401

        user = database.get_user(session['user_id'])
        request.user = user
        return f(*args, **kwargs)
    return decorated

def admin_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        # Fixed: Verify role from database
        if not hasattr(request, 'user') or request.user.role != 'admin':
            return "Forbidden", 403
        return f(*args, **kwargs)
    return decorated

@app.route('/admin/action', methods=['POST'])
@login_required
@admin_required
def secure_admin_action():
    # Role verified from server-side session and database
    perform_admin_action()

@app.route('/api/data')
def secure_api():
    # Fixed: Don't trust X-Forwarded-For directly
    # Use trusted proxy configuration or verify differently

    # Option 1: Use actual remote address from trusted proxy
    client_ip = get_trusted_client_ip(request)

    # Option 2: Use authentication instead of IP
    api_key = request.headers.get('X-API-Key')
    if not verify_api_key(api_key):
        return "Unauthorized", 401

    return get_sensitive_data()
// Fixed: Return defensive copy of array
public class SecurePermissions {

    private final String[] adminUsers = {"admin", "root", "superuser"};

    // Fixed: Return copy of array
    public String[] getAdminUsers() {
        return Arrays.copyOf(adminUsers, adminUsers.length);
    }

    // Fixed: Or return unmodifiable view
    public List<String> getAdminUsersList() {
        return Collections.unmodifiableList(Arrays.asList(adminUsers));
    }

    // Fixed: Better - don't expose internal data at all
    public boolean isAdmin(String username) {
        for (String admin : adminUsers) {
            if (admin.equals(username)) {
                return true;
            }
        }
        return false;
    }
}
<?php
// Fixed: Proper authentication without trusting user input
function secure_authenticate() {
    session_start();

    // Fixed: Check session, not user-controllable variables
    if (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) {
        grant_access();
    } else {
        require_login();
    }
}

// Fixed: Don't use PHP_SELF, use known action
function secure_form() {
    // Fixed: Use explicit, known action URL
    $action = '/process_form.php';
    echo '<form action="' . htmlspecialchars($action, ENT_QUOTES) . '">';
}

// Fixed: Sign data that must traverse client
function create_signed_form_data($data) {
    $json = json_encode($data);
    $signature = hash_hmac('sha256', $json, SECRET_KEY);

    return base64_encode($json . '|' . $signature);
}

function verify_signed_form_data($encoded) {
    $decoded = base64_decode($encoded);
    list($json, $signature) = explode('|', $decoded, 2);

    // Fixed: Verify signature
    $expected = hash_hmac('sha256', $json, SECRET_KEY);
    if (!hash_equals($expected, $signature)) {
        throw new SecurityException("Data tampered");
    }

    return json_decode($json, true);
}
?>
<!-- Fixed: Signed hidden fields -->
<form action="/checkout" method="POST">
    <!-- Product info signed by server -->
    <input type="hidden" name="signed_data"
           value="eyJwcm9kdWN0X2lkIjoxMjMsInByaWNlIjo5OS45OX0=|abc123signature">

    <input type="text" name="quantity" value="1">
    <input type="submit" value="Purchase">
</form>

<!-- Server verifies signature before processing -->

CVE Examples

  • CVE-2002-1757 - Authentication relied on PHP_SELF variable that could be manipulated by attackers.
  • CVE-2005-1905 - Privilege escalation through modification of code addresses assumed to be immutable in drivers.

References

  1. MITRE Corporation. "CWE-471: Modification of Assumed-Immutable Data (MAID)." https://cwe.mitre.org/data/definitions/471.html
  2. OWASP. "Parameter Tampering." https://owasp.org/www-community/attacks/Parameter_Tampering
  3. CAPEC-384. "Application API Message Manipulation via Man-in-the-Middle."