Skip to content

CWE-642: External Control of Critical State Data

Overview

External control of critical state data occurs when an application allows untrusted input to directly control state variables that determine program behavior, authentication status, authorization levels, or security decisions. This vulnerability arises when developers use client-side storage (cookies, hidden form fields, URL parameters) to store security-critical state like user roles, permissions, prices, or session attributes without server-side validation or cryptographic protection. Attackers manipulate these client-controlled values to bypass security controls, escalate privileges, manipulate business logic, or gain unauthorized access.

OWASP Classification

A06:2025 - Vulnerable and Outdated Components (Security Design Flaws)

Note: This CWE relates more closely to insecure design patterns than vulnerable components, representing architectural security flaws.

Risk

High: Directly enables privilege escalation, authentication bypass, price manipulation, authorization circumvention, and business logic attacks. Client-controlled state variables create fundamental security failures that are easily exploitable without specialized tools.

Remediation Steps

Core principle: Never trust client-side data for security decisions; store all security-critical state server-side and validate any client-provided state against authoritative server records.

Store Critical State Server-Side

VULNERABLE - Client-controlled role:

# Flask - role in cookie
@app.route('/admin')
def admin():
    role = request.cookies.get('role')
    if role == 'admin':
        return render_template('admin.html')
    return abort(403)

SECURE - Server-side session:

# Flask with server-side session
from flask_session import Session

app.config['SESSION_TYPE'] = 'redis'  # Or filesystem, sqlalchemy
Session(app)

@app.route('/login', methods=['POST'])
def login():
    user = authenticate(request.form['username'], request.form['password'])
    if user:
        session['user_id'] = user.id
        session['role'] = user.role  # Stored server-side
        return redirect('/dashboard')

@app.route('/admin')
def admin():
    # Session data is server-side, cannot be tampered
    if session.get('role') == 'admin':
        return render_template('admin.html')
    return abort(403)

Validate Client State Against Server

VULNERABLE - Price from form:

// Node.js/Express - price controlled by client
app.post('/checkout', async (req, res) => {
    const { productId, price } = req.body;  // Attacker can set price
    await processPayment(price);
    res.json({ success: true });
});

SECURE - Validate price server-side:

app.post('/checkout', async (req, res) => {
    const { productId, quantity } = req.body;

    // Fetch price from database (authoritative source)
    const product = await db.products.findById(productId);
    const actualPrice = product.price * quantity;

    // Never trust client-provided price
    await processPayment(actualPrice);
    res.json({ success: true, amount: actualPrice });
});

Use Cryptographic Protection for Client State

When client-side state is necessary (e.g., stateless authentication):

JWT with signature verification:

# Python with PyJWT
import jwt
import datetime

SECRET_KEY = os.environ['JWT_SECRET']

def create_token(user):
    payload = {
        'user_id': user.id,
        'role': user.role,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token

@app.route('/admin')
def admin():
    token = request.cookies.get('token')
    try:
        # Signature verification prevents tampering
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        if payload['role'] == 'admin':
            return render_template('admin.html')
    except jwt.InvalidTokenError:
        abort(401)
    abort(403)

Implement Access Control at Resource Level

VULNERABLE - Permission in URL:

// PHP - permission in URL parameter
$canEdit = $_GET['can_edit'];  // Attacker sets to 'true'
if ($canEdit === 'true') {
    updateDocument($_POST['content']);
}

SECURE - Check permissions server-side:

session_start();
$userId = $_SESSION['user_id'];
$documentId = $_POST['document_id'];

// Check actual permissions in database
$canEdit = $db->query(
    "SELECT can_edit FROM document_permissions 
     WHERE user_id = ? AND document_id = ?",
    [$userId, $documentId]
)->fetch();

if ($canEdit) {
    updateDocument($documentId, $_POST['content']);
} else {
    http_response_code(403);
    exit('Access denied');
}

Avoid Hidden Form Fields for Security Decisions

VULNERABLE - Role in hidden field:

<form method="POST" action="/update-profile">
    <input type="hidden" name="role" value="user">
    <input type="text" name="email">
    <button>Update</button>
</form>
# Attacker changes role to 'admin' in request
role = request.form['role']  # Never trust this
user.role = role  # VULNERABLE

SECURE - Server-side authorization:

@app.route('/update-profile', methods=['POST'])
@login_required
def update_profile():
    user = get_current_user()

    # Only allow updating safe fields
    user.email = request.form['email']

    # Role changes require separate admin endpoint
    # Never accept role from client input
    db.session.commit()

Use Indirect Object References

VULNERABLE - Direct IDs:

// Client controls which account to access
app.get('/account/:accountId', (req, res) => {
    const account = await Account.findById(req.params.accountId);
    res.json(account);  // Any account accessible
});

SECURE - Validate ownership:

app.get('/account/:accountId', requireAuth, async (req, res) => {
    const account = await Account.findById(req.params.accountId);

    // Verify user owns this account
    if (account.userId !== req.user.id) {
        return res.status(403).json({ error: 'Access denied' });
    }

    res.json(account);
});

Dynamic Scan Guidance

For guidance on remediating this CWE when detected by dynamic (DAST) scanners:

Additional Resources