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:
- Dynamic Scan Guidance - Analyzing DAST findings and mapping to source code