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
| Impact | Details |
|---|---|
| Authorization | Scope: Privilege Escalation Users gain unauthorized access levels. |
| Integrity | Scope: State Manipulation Application state corrupted by attackers. |
| Authentication | Scope: 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
-
MITRE. "CWE-642: External Control of Critical State Data." https://cwe.mitre.org/data/definitions/642.html
-
OWASP. "Insecure Client-Side Storage."