External Control of Critical State Data

Description

External Control of Critical State Data occurs when security-critical application state (user roles, permissions, session data, authentication status) is stored in locations controlled or modifiable by users. This includes cookies, hidden form fields, client-side storage, URL parameters, or unencrypted client-accessible data. Attackers can modify this state to escalate privileges or bypass security controls.

Risk

Privilege escalation by modifying role/permission data. Authentication bypass by manipulating session state. Access to restricted features by changing feature flags. Price manipulation in e-commerce by modifying client-stored prices. Bypassing business logic by altering workflow state. Complete account takeover through state manipulation.

Solution

Store critical state server-side only. Never trust client-provided state data. Use cryptographically signed tokens for any client-stored data. Verify state integrity on every server request. Implement server-side session management. Re-validate permissions from authoritative source on each request.

Common Consequences

ImpactDetails
AuthorizationScope: Privilege Escalation

Users gain unauthorized access levels.
IntegrityScope: State Manipulation

Application state corrupted by attackers.
AuthenticationScope: Bypass

Auth status manipulated to gain access.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: Role stored in cookie
@WebServlet("/dashboard")
public class VulnerableDashboard extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // Reading role from cookie - user controllable!
        Cookie[] cookies = req.getCookies();
        String role = "user";  // default

        for (Cookie c : cookies) {
            if ("userRole".equals(c.getName())) {
                role = c.getValue();  // Attacker sets to "admin"
            }
        }

        if ("admin".equals(role)) {
            showAdminPanel(resp);  // Privilege escalation!
        }
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        // Role from hidden form field - user can modify
        String role = req.getParameter("role");  // Hidden field
        if ("admin".equals(role)) {
            performAdminAction(req);
        }
    }
}

// VULNERABLE: Authentication status in client
public class VulnerableAuth {

    public void login(HttpServletResponse resp, String username) {
        // Auth status stored in plain cookie
        Cookie authCookie = new Cookie("authenticated", "true");
        Cookie userCookie = new Cookie("username", username);
        Cookie adminCookie = new Cookie("isAdmin", "false");

        resp.addCookie(authCookie);
        resp.addCookie(userCookie);
        resp.addCookie(adminCookie);
        // Attacker modifies isAdmin=true
    }
}
# VULNERABLE: Flask with client-side state
from flask import Flask, request, make_response
app = Flask(__name__)

@app.route('/checkout', methods=['POST'])
def checkout_vulnerable():
    # Price from hidden form field - user can modify!
    price = float(request.form['total_price'])  # Was $99.99
    # Attacker changes to $0.01
    process_payment(price)
    return "Order placed!"

@app.route('/profile')
def profile_vulnerable():
    # User ID from cookie - anyone can change
    user_id = request.cookies.get('user_id')  # Attacker sets to admin ID
    user_data = db.get_user(user_id)
    return render_template('profile.html', user=user_data)

@app.route('/admin')
def admin_vulnerable():
    # Admin check from client cookie
    is_admin = request.cookies.get('is_admin') == 'true'
    if is_admin:  # Attacker sets cookie
        return render_admin_panel()
    return "Access denied", 403

# VULNERABLE: Workflow state in URL
@app.route('/order/<order_id>/status/<status>')
def update_order_vulnerable(order_id, status):
    # Status from URL - user can navigate directly
    # /order/123/status/shipped - skip payment!
    db.update_order_status(order_id, status)
    return f"Order {order_id} is now {status}"
// VULNERABLE: Client-side role management
class VulnerableApp {
    constructor() {
        // Role stored in localStorage - user can modify via DevTools
        this.role = localStorage.getItem('userRole') || 'guest';
        this.isAdmin = localStorage.getItem('isAdmin') === 'true';
    }

    checkPermission(action) {
        // Client-side permission check
        if (this.role === 'admin' || this.isAdmin) {
            return true;  // Attacker sets localStorage.isAdmin = 'true'
        }
        return false;
    }

    async deleteUser(userId) {
        if (this.checkPermission('delete_user')) {
            // Server trusts client permission check!
            await fetch(`/api/users/${userId}`, { method: 'DELETE' });
        }
    }
}

// VULNERABLE: Hidden form fields
/*
<form action="/transfer" method="POST">
    <input type="hidden" name="fromAccount" value="12345">
    <input type="hidden" name="maxLimit" value="1000">
    <!-- Attacker modifies maxLimit to 999999 -->
    <input type="text" name="amount">
    <button type="submit">Transfer</button>
</form>
*/

app.post('/transfer', (req, res) => {
    const { fromAccount, maxLimit, amount } = req.body;
    // Trusting maxLimit from form!
    if (amount <= maxLimit) {
        transferFunds(fromAccount, amount);
    }
});

// VULNERABLE: JWT without proper validation
app.use((req, res, next) => {
    const token = req.cookies.token;
    if (token) {
        // Just decoding, not verifying signature!
        const decoded = jwt.decode(token);  // NOT verify()
        req.user = decoded;  // Attacker forges token
    }
    next();
});
<?php
// VULNERABLE: Session data from cookie
$user_role = $_COOKIE['user_role'];  // User controlled
if ($user_role === 'administrator') {
    include 'admin_panel.php';  // Privilege escalation
}

// VULNERABLE: Price manipulation
$price = $_POST['item_price'];  // From hidden field
$quantity = $_POST['quantity'];
$total = $price * $quantity;  // Attacker set price to 0.01
processPayment($total);

// VULNERABLE: User ID from GET parameter
$viewing_user = $_GET['user_id'];  // Anyone can view any user
$user_data = fetchUserData($viewing_user);
display($user_data);

// VULNERABLE: Discount code validation on client
$discount = $_POST['discount_percentage'];  // Hidden field
// Attacker changes from 10 to 100
$final_price = $original_price * (1 - $discount / 100);

// VULNERABLE: Account number from form
$from_account = $_POST['from_account'];  // Hidden field
$to_account = $_POST['to_account'];
$amount = $_POST['amount'];
// Attacker changes from_account to someone else's
transferMoney($from_account, $to_account, $amount);
?>

Fixed Code

// SAFE: Server-side session state management
@WebServlet("/dashboard")
public class SafeDashboard extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // Get role from server-side session only
        HttpSession session = req.getSession(false);
        if (session == null) {
            resp.sendRedirect("/login");
            return;
        }

        // Role from session, never from client
        User user = (User) session.getAttribute("user");
        if (user == null) {
            resp.sendRedirect("/login");
            return;
        }

        // Verify against database for critical operations
        User freshUser = userService.findById(user.getId());
        if (freshUser.hasRole("admin")) {
            showAdminPanel(resp);
        }
    }
}

// SAFE: Signed tokens for client state
public class SafeAuth {

    private final String secretKey = System.getenv("JWT_SECRET");

    public String createAuthToken(User user) {
        return Jwts.builder()
            .setSubject(user.getId().toString())
            .claim("roles", user.getRoles())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 3600000))
            .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
            .compact();
    }

    public User validateToken(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                .setSigningKey(secretKey.getBytes())
                .build()
                .parseClaimsJws(token)
                .getBody();

            // Verify user still exists and has claimed roles
            User user = userService.findById(Long.parseLong(claims.getSubject()));
            if (user == null) {
                throw new UnauthorizedException();
            }

            return user;
        } catch (JwtException e) {
            throw new UnauthorizedException("Invalid token");
        }
    }
}
# SAFE: Server-side state management
from flask import Flask, session, request, redirect
from functools import wraps
import hmac
import hashlib

app = Flask(__name__)
app.secret_key = os.environ['SECRET_KEY']

def require_admin(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        # Check server-side session
        user_id = session.get('user_id')
        if not user_id:
            return redirect('/login')

        # Verify against database
        user = db.get_user(user_id)
        if not user or 'admin' not in user.roles:
            return "Access denied", 403

        return f(*args, **kwargs)
    return decorated

@app.route('/checkout', methods=['POST'])
def checkout_safe():
    # Get price from server, not client
    cart = get_user_cart(session['user_id'])
    total_price = calculate_total(cart)  # Server calculates
    process_payment(total_price)
    return "Order placed!"

@app.route('/profile')
def profile_safe():
    # Use authenticated session user
    user_id = session.get('user_id')
    if not user_id:
        return redirect('/login')

    user_data = db.get_user(user_id)
    return render_template('profile.html', user=user_data)

@app.route('/admin')
@require_admin
def admin_safe():
    return render_admin_panel()

# SAFE: Server-controlled workflow
@app.route('/order/<order_id>/ship', methods=['POST'])
@require_admin
def ship_order_safe(order_id):
    order = db.get_order(order_id)

    # Verify order state on server
    if order.status != 'paid':
        return "Cannot ship unpaid order", 400

    db.update_order_status(order_id, 'shipped')
    return f"Order {order_id} shipped"
// SAFE: Server-side state with signed tokens
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;

// Server-side session store
const sessionStore = new Map();

class SafeAuth {
    createSession(user) {
        const sessionId = crypto.randomUUID();
        sessionStore.set(sessionId, {
            userId: user.id,
            roles: user.roles,
            createdAt: Date.now()
        });

        // Return signed session ID
        return jwt.sign({ sessionId }, SECRET, { expiresIn: '1h' });
    }

    validateSession(token) {
        try {
            const { sessionId } = jwt.verify(token, SECRET);
            const session = sessionStore.get(sessionId);

            if (!session) {
                throw new Error('Session not found');
            }

            return session;
        } catch (error) {
            throw new UnauthorizedError('Invalid session');
        }
    }
}

// SAFE: Server-side permission checks
app.post('/transfer', async (req, res) => {
    const session = auth.validateSession(req.cookies.token);

    // Get account from session, not form
    const fromAccount = await db.getUserAccount(session.userId);

    // Get transfer limit from database, not form
    const user = await db.getUser(session.userId);
    const maxLimit = user.transferLimit;

    const { toAccount, amount } = req.body;

    if (amount > maxLimit) {
        return res.status(400).json({ error: 'Exceeds limit' });
    }

    await transferFunds(fromAccount.id, toAccount, amount);
    res.json({ success: true });
});

// SAFE: Price from server
app.post('/checkout', async (req, res) => {
    const session = auth.validateSession(req.cookies.token);
    const cart = await db.getUserCart(session.userId);

    // Calculate price server-side
    const total = cart.items.reduce((sum, item) => {
        const product = db.getProduct(item.productId);
        return sum + (product.price * item.quantity);
    }, 0);

    await processPayment(total);
    res.json({ success: true, total });
});
<?php
// SAFE: Server-side session management
session_start();

// Role from session, not cookie
function getUserRole() {
    if (!isset($_SESSION['user_id'])) {
        return null;
    }

    // Verify against database
    global $db;
    $user = $db->query(
        "SELECT role FROM users WHERE id = ?",
        [$_SESSION['user_id']]
    )->fetch();

    return $user ? $user['role'] : null;
}

// SAFE: Admin check
if (getUserRole() === 'administrator') {
    include 'admin_panel.php';
}

// SAFE: Price from server
function checkout() {
    $cart = getCartFromSession();
    $total = 0;

    foreach ($cart as $item) {
        // Get price from database, not client
        $product = getProductFromDatabase($item['product_id']);
        $total += $product['price'] * $item['quantity'];
    }

    processPayment($total);
}

// SAFE: User operations on authenticated user only
function viewProfile() {
    if (!isset($_SESSION['user_id'])) {
        header('Location: /login');
        exit;
    }

    $user_data = fetchUserData($_SESSION['user_id']);
    display($user_data);
}

// SAFE: Transfer with server-side validation
function transfer($to_account, $amount) {
    if (!isset($_SESSION['user_id'])) {
        throw new Exception('Not authenticated');
    }

    // Get FROM account from session user, not form
    $from_account = getUserPrimaryAccount($_SESSION['user_id']);

    // Get limit from database
    $limit = getUserTransferLimit($_SESSION['user_id']);

    if ($amount > $limit) {
        throw new Exception('Exceeds limit');
    }

    transferMoney($from_account, $to_account, $amount);
}
?>

Exploited in the Wild

E-commerce Price Manipulation

Hidden field modifications for free purchases.

Privilege Escalation

Cookie-based role manipulation.

Account Takeover

Client-stored user IDs enabling impersonation.


Tools to test/exploit

  • Browser Developer Tools for cookie/storage manipulation.

  • Burp Suite — parameter tampering.

  • OWASP ZAP proxy.


CVE Examples

  • CVE-2017-12615: Apache Tomcat using client-provided state.

  • Numerous privilege escalation CVEs via client state.


References

  1. MITRE. "CWE-642: External Control of Critical State Data." https://cwe.mitre.org/data/definitions/642.html

  2. OWASP. "Insecure Client-Side Storage."