External Control of Assumed-Immutable Web Parameter
Description
External Control of Assumed-Immutable Web Parameter occurs when a web application uses input from HTTP parameters that the developer assumes cannot be modified by end users, but which can actually be manipulated. This includes hidden form fields, cookies, HTTP headers, URL parameters, and any client-side data that the server trusts without validation. Attackers can modify these "hidden" values to bypass security controls, change prices, alter user roles, or manipulate application logic.
Risk
Developers often assume that hidden fields, cookies, or client-generated values are safe because users "can't see" or "can't change" them. However, all client-side data is trivially modifiable using browser developer tools, proxy tools like Burp Suite, or simple curl commands. Price manipulation, privilege escalation, and business logic bypasses are common results. E-commerce applications have suffered significant financial losses from hidden price field manipulation.
Solution
Never trust client-side data for security decisions. Validate all input on the server side regardless of its source. Use server-side session storage for sensitive values instead of hidden fields or cookies. Implement integrity checks (HMAC signatures) for client-stored state that must round-trip. Use server-side price lookups rather than client-submitted prices. Implement proper authorization checks that don't rely on client-supplied role information. Consider all HTTP parameters as potentially malicious.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Data Manipulation Attackers can modify prices, quantities, and other transaction data. |
| Access Control | Scope: Privilege Escalation Hidden role fields allow users to grant themselves elevated privileges. |
| Financial | Scope: Fraud Price manipulation leads to direct financial losses. |
Example Code + Solution Code
Vulnerable Code
<!-- VULNERABLE: Hidden field for price -->
<form action="/checkout" method="POST">
<input type="hidden" name="product_id" value="12345">
<input type="hidden" name="price" value="99.99"> <!-- Attacker changes to 0.01 -->
<input type="hidden" name="user_role" value="customer"> <!-- Attacker changes to admin -->
<input type="number" name="quantity" value="1">
<button type="submit">Buy Now</button>
</form>
<!-- VULNERABLE: Hidden field for discount -->
<form action="/apply-discount" method="POST">
<input type="hidden" name="discount_percent" value="0"> <!-- Attacker changes to 100 -->
<button type="submit">Apply Discount</button>
</form>
# VULNERABLE: Trusting hidden form field for price
from flask import Flask, request
@app.route('/checkout', methods=['POST'])
def checkout_vulnerable():
product_id = request.form['product_id']
price = float(request.form['price']) # Trusting client-submitted price!
quantity = int(request.form['quantity'])
total = price * quantity
# Process payment with manipulated price
charge_customer(total)
return f"Charged ${total}"
# VULNERABLE: Trusting cookie for user role
@app.route('/admin/users')
def admin_users_vulnerable():
# Role from cookie - easily manipulated!
role = request.cookies.get('user_role')
if role == 'admin':
return get_all_users()
return 'Access denied', 403
# VULNERABLE: Trusting hidden field for user ID
@app.route('/update-profile', methods=['POST'])
def update_profile_vulnerable():
# User ID from hidden field - can be changed to any user!
user_id = request.form['user_id']
new_email = request.form['email']
# Updates ANY user's email!
User.query.get(user_id).email = new_email
db.session.commit()
return 'Profile updated'
// VULNERABLE: Java servlet trusting hidden fields
@WebServlet("/checkout")
public class VulnerableCheckout extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String productId = request.getParameter("product_id");
double price = Double.parseDouble(request.getParameter("price")); // Client price!
int quantity = Integer.parseInt(request.getParameter("quantity"));
double total = price * quantity;
// Process with manipulated price
paymentService.charge(total);
response.getWriter().println("Charged: $" + total);
}
}
// VULNERABLE: Trusting cookie for authorization
@WebServlet("/admin")
public class VulnerableAdmin extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Role from cookie!
Cookie[] cookies = request.getCookies();
String role = null;
for (Cookie cookie : cookies) {
if ("user_role".equals(cookie.getName())) {
role = cookie.getValue();
break;
}
}
if ("admin".equals(role)) {
// Grant admin access based on cookie!
showAdminPanel(response);
} else {
response.sendError(403);
}
}
}
// VULNERABLE: Express trusting client data
app.post('/checkout', (req, res) => {
const { product_id, price, quantity } = req.body;
// Trusting client-submitted price!
const total = parseFloat(price) * parseInt(quantity);
chargeCustomer(total);
res.json({ total });
});
// VULNERABLE: Client-side role check replicated on server
app.get('/api/admin/data', (req, res) => {
// Trusting header set by client-side JavaScript!
const isAdmin = req.headers['x-user-admin'] === 'true';
if (isAdmin) {
res.json(getAdminData());
} else {
res.status(403).json({ error: 'Access denied' });
}
});
// VULNERABLE: Frontend code setting "secure" values
// frontend.js
function checkout(productId, quantity) {
fetch('/checkout', {
method: 'POST',
body: JSON.stringify({
product_id: productId,
price: document.getElementById('price').value, // From hidden field!
quantity: quantity
})
});
}
Fixed Code
# SAFE: Server-side price lookup
from flask import Flask, request, session
from functools import wraps
@app.route('/checkout', methods=['POST'])
def checkout_safe():
product_id = request.form['product_id']
quantity = int(request.form['quantity'])
# Get price from database, not client!
product = Product.query.get(product_id)
if not product:
return 'Product not found', 404
price = product.price # Server-side price!
total = price * quantity
charge_customer(total)
return f"Charged ${total}"
# SAFE: Server-side session for role
def require_role(role):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Get role from server-side session, not cookie
if session.get('user_role') != role:
return 'Access denied', 403
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/admin/users')
@require_role('admin')
def admin_users_safe():
return get_all_users()
# SAFE: Using authenticated user ID from session
@app.route('/update-profile', methods=['POST'])
def update_profile_safe():
# User ID from server-side session!
user_id = session.get('user_id')
if not user_id:
return 'Not authenticated', 401
new_email = request.form['email']
# Only update current user's profile
User.query.get(user_id).email = new_email
db.session.commit()
return 'Profile updated'
# SAFE: HMAC-signed hidden fields for integrity
import hmac
import hashlib
import json
import base64
SECRET_KEY = os.environ['FORM_SIGNING_KEY']
def sign_form_data(data):
"""Sign form data with HMAC."""
json_data = json.dumps(data, sort_keys=True)
signature = hmac.new(
SECRET_KEY.encode(),
json_data.encode(),
hashlib.sha256
).hexdigest()
return base64.b64encode(json_data.encode()).decode(), signature
def verify_form_data(encoded_data, signature):
"""Verify signed form data."""
json_data = base64.b64decode(encoded_data).decode()
expected_sig = hmac.new(
SECRET_KEY.encode(),
json_data.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_sig, signature):
return None
return json.loads(json_data)
@app.route('/product/<product_id>')
def show_product(product_id):
product = Product.query.get(product_id)
# Sign the form data
form_data = {'product_id': product_id, 'price': str(product.price)}
encoded, signature = sign_form_data(form_data)
return render_template('product.html',
product=product,
form_data=encoded,
signature=signature)
@app.route('/checkout', methods=['POST'])
def checkout_signed():
encoded_data = request.form['form_data']
signature = request.form['signature']
# Verify integrity
data = verify_form_data(encoded_data, signature)
if not data:
return 'Invalid form data', 400
# Use verified data
product_id = data['product_id']
price = float(data['price'])
quantity = int(request.form['quantity'])
# Additional server-side verification
product = Product.query.get(product_id)
if str(product.price) != data['price']:
return 'Price mismatch', 400
total = price * quantity
charge_customer(total)
return f"Charged ${total}"
// SAFE: Server-side price lookup in Java
@WebServlet("/checkout")
public class SecureCheckout extends HttpServlet {
@Inject
private ProductRepository productRepository;
@Inject
private PaymentService paymentService;
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String productId = request.getParameter("product_id");
int quantity = Integer.parseInt(request.getParameter("quantity"));
// Get price from database!
Product product = productRepository.findById(productId)
.orElseThrow(() -> new NotFoundException("Product not found"));
BigDecimal price = product.getPrice(); // Server-side!
BigDecimal total = price.multiply(BigDecimal.valueOf(quantity));
paymentService.charge(total);
response.getWriter().println("Charged: $" + total);
}
}
// SAFE: Session-based authorization
@WebServlet("/admin")
public class SecureAdmin extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null) {
response.sendError(401);
return;
}
// Role from server-side session!
String role = (String) session.getAttribute("user_role");
if (!"admin".equals(role)) {
response.sendError(403);
return;
}
showAdminPanel(response);
}
}
// SAFE: JWT for stateless verification
@WebFilter("/*")
public class JwtAuthFilter implements Filter {
private final String jwtSecret = System.getenv("JWT_SECRET");
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
try {
// Verify JWT - can't be tampered
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtSecret.getBytes())
.build()
.parseClaimsJws(token.substring(7))
.getBody();
request.setAttribute("userId", claims.getSubject());
request.setAttribute("role", claims.get("role"));
} catch (JwtException e) {
((HttpServletResponse) res).sendError(401);
return;
}
}
chain.doFilter(req, res);
}
}
// SAFE: Server-side price lookup in Node.js
app.post('/checkout', async (req, res) => {
const { product_id, quantity } = req.body;
// Get price from database!
const product = await Product.findById(product_id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const price = product.price; // Server-side!
const total = price * parseInt(quantity);
await chargeCustomer(total);
res.json({ total });
});
// SAFE: Session-based role verification
app.get('/api/admin/data', requireAuth, (req, res) => {
// Role from server-side session
if (req.session.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
res.json(getAdminData());
});
// SAFE: Signed form data
const crypto = require('crypto');
function signFormData(data) {
const json = JSON.stringify(data);
const signature = crypto
.createHmac('sha256', process.env.FORM_SECRET)
.update(json)
.digest('hex');
return {
data: Buffer.from(json).toString('base64'),
signature
};
}
function verifyFormData(encodedData, signature) {
const json = Buffer.from(encodedData, 'base64').toString();
const expectedSig = crypto
.createHmac('sha256', process.env.FORM_SECRET)
.update(json)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(signature)
)) {
return null;
}
return JSON.parse(json);
}
// Template rendering with signed data
app.get('/product/:id', async (req, res) => {
const product = await Product.findById(req.params.id);
const signed = signFormData({
product_id: product.id,
price: product.price.toString()
});
res.render('product', { product, ...signed });
});
// Verify on submission
app.post('/checkout', async (req, res) => {
const { data, signature, quantity } = req.body;
const verified = verifyFormData(data, signature);
if (!verified) {
return res.status(400).json({ error: 'Invalid form data' });
}
// Additional database verification
const product = await Product.findById(verified.product_id);
if (product.price.toString() !== verified.price) {
return res.status(400).json({ error: 'Price changed' });
}
const total = parseFloat(verified.price) * parseInt(quantity);
await chargeCustomer(total);
res.json({ total });
});
Exploited in the Wild
E-commerce Price Manipulation
Multiple online stores have suffered financial losses when attackers modified hidden price fields to purchase items for minimal amounts.
Privilege Escalation via Cookies
Applications storing role information in cookies have been exploited to gain administrative access.
Business Logic Bypasses
Attackers have manipulated hidden fields controlling discounts, shipping costs, and transaction limits.
Tools to test/exploit
-
Burp Suite — intercept and modify HTTP parameters.
-
Browser Developer Tools — edit hidden fields directly.
-
curl — send requests with arbitrary parameters.
-
OWASP ZAP — automated parameter tampering.
CVE Examples
-
CVE-2021-22893 — Pulse Secure parameter manipulation.
-
CVE-2019-16113 — Bludit CMS hidden field manipulation.
-
Numerous application-specific vulnerabilities.
References
-
MITRE. "CWE-472: External Control of Assumed-Immutable Web Parameter." https://cwe.mitre.org/data/definitions/472.html
-
OWASP. "Testing for HTTP Parameter Pollution." https://owasp.org/www-project-web-security-testing-guide/