Trusting HTTP Permission Methods on the Server Side
Description
Trusting HTTP Permission Methods on the Server Side is a vulnerability where a web server or application incorrectly assumes that HTTP GET requests cannot modify server-side resources or state. The HTTP specification defines GET as a "safe" method that should only retrieve data without side effects. However, nothing technically prevents applications from implementing GET requests that create, update, or delete data. When servers rely on HTTP method restrictions alone for access control—allowing GET but blocking POST for certain resources—attackers can bypass these controls by using GET requests to perform state-changing operations.
Risk
Relying on HTTP methods for security creates significant access control vulnerabilities. Attackers can modify, create, or delete resources by crafting GET requests to endpoints that accept them. REST APIs that use GET for modifications are particularly vulnerable. Administrative functions protected only by method restrictions can be invoked. Cross-Site Request Forgery (CSRF) attacks become easier since GET requests can be triggered via images or links. Caching proxies may cache dangerous operations, repeating them unintentionally. Search engine crawlers following links may trigger destructive operations. The false sense of security leads developers to neglect proper authorization checks.
Solution
Never rely on HTTP methods alone for access control. Implement proper authentication and authorization checks for every operation regardless of the HTTP method used. Follow REST conventions: use GET only for read operations, POST/PUT for creation and updates, DELETE for removal. Configure web application firewalls to block state-changing GET requests to sensitive endpoints. Implement CSRF protection on all state-changing operations. Review and audit API designs to ensure HTTP methods align with their intended semantics. Use POST for any operation with side effects, even if it could technically work as GET.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Gain Privileges or Assume Identity - Attackers can bypass method-based access controls to perform privileged operations. |
| Integrity | Scope: Integrity Modify Application Data - State-changing operations via GET allow unauthorized data modification. |
| Confidentiality | Scope: Confidentiality Read Application Data - Sensitive data retrieval operations may be exposed through GET requests logged in various places. |
Example Code
Vulnerable Code
// Vulnerable: Trusting HTTP method for access control
@WebServlet("/admin/*")
public class VulnerableAdminServlet extends HttpServlet {
// Vulnerable: Assuming GET is safe, allowing it without auth check
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String action = request.getParameter("action");
// Vulnerable: State-changing operations via GET
if ("delete".equals(action)) {
String userId = request.getParameter("userId");
userService.deleteUser(userId); // Dangerous operation via GET!
response.getWriter().println("User deleted");
}
else if ("promote".equals(action)) {
String userId = request.getParameter("userId");
userService.promoteToAdmin(userId); // Privilege escalation via GET!
response.getWriter().println("User promoted");
}
}
// POST is protected but GET allows same operations
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Check admin authentication for POST only
if (!isAdmin(request)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
doGet(request, response); // Delegates to vulnerable GET handler
}
}
// Attack: Simply use GET to bypass POST restrictions
// http://example.com/admin?action=delete&userId=12345
// http://example.com/admin?action=promote&userId=attacker123
# Vulnerable: Flask app trusting HTTP methods
from flask import Flask, request, redirect
app = Flask(__name__)
# Vulnerable: State-changing operation via GET
@app.route('/account/close', methods=['GET'])
def close_account():
# Vulnerable: No auth check, assumes GET is safe
user_id = request.args.get('user_id')
db.execute("DELETE FROM accounts WHERE user_id = ?", [user_id])
return "Account closed"
# Vulnerable: Transfer money via GET
@app.route('/transfer', methods=['GET'])
def transfer():
# Vulnerable: Financial operation via GET
from_account = request.args.get('from')
to_account = request.args.get('to')
amount = request.args.get('amount')
# This can be triggered by visiting a link or loading an image:
# <img src="http://bank.com/transfer?from=victim&to=attacker&amount=10000">
db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?",
[amount, from_account])
db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?",
[amount, to_account])
return redirect('/success')
# Vulnerable: Web server config trusting method restrictions
# Apache config that's insufficient:
# <Location /admin>
# <LimitExcept GET>
# Require user admin
# </LimitExcept>
# </Location>
# This allows GET without authentication!
<?php
// Vulnerable: PHP script trusting request method
// Vulnerable: Only checking method, not authorization
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Check admin session for POST
if (!isset($_SESSION['is_admin']) || !$_SESSION['is_admin']) {
die('Access denied');
}
}
// Vulnerable: GET requests bypass the admin check above
$action = $_GET['action'] ?? $_POST['action'] ?? '';
switch ($action) {
case 'delete_user':
// Vulnerable: Can be triggered via GET, bypassing admin check
$userId = $_GET['user_id'] ?? $_POST['user_id'];
deleteUser($userId);
break;
case 'reset_password':
// Vulnerable: Password reset via GET
$userId = $_GET['user_id'] ?? $_POST['user_id'];
resetPassword($userId);
break;
}
Fixed Code
// Fixed: Proper authentication for all operations
@WebServlet("/admin/*")
public class SecureAdminServlet extends HttpServlet {
// Fixed: No state-changing operations in GET
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// GET only returns information, never modifies state
if (!isAuthenticated(request) || !isAdmin(request)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
String action = request.getParameter("action");
// Fixed: GET only for read operations
if ("list".equals(action)) {
List<User> users = userService.listUsers();
writeJsonResponse(response, users);
}
else if ("view".equals(action)) {
String userId = request.getParameter("userId");
User user = userService.getUser(userId);
writeJsonResponse(response, user);
}
else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
"Invalid action for GET request");
}
}
// Fixed: State-changing operations only via POST with auth and CSRF
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Fixed: Always check authentication
if (!isAuthenticated(request) || !isAdmin(request)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
// Fixed: Verify CSRF token
if (!verifyCSRFToken(request)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN,
"Invalid CSRF token");
return;
}
String action = request.getParameter("action");
if ("delete".equals(action)) {
String userId = request.getParameter("userId");
// Additional authorization check
if (!canDeleteUser(request, userId)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
userService.deleteUser(userId);
writeJsonResponse(response, Map.of("status", "deleted"));
}
else if ("promote".equals(action)) {
String userId = request.getParameter("userId");
userService.promoteToAdmin(userId);
writeJsonResponse(response, Map.of("status", "promoted"));
}
}
// Fixed: DELETE method for delete operations
@Override
protected void doDelete(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
if (!isAuthenticated(request) || !isAdmin(request)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
String userId = request.getParameter("userId");
userService.deleteUser(userId);
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
}
# Fixed: Flask app with proper method handling
from flask import Flask, request, redirect, session, abort
from functools import wraps
app = Flask(__name__)
app.secret_key = 'secure-secret-key'
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if 'user_id' not in session:
abort(401)
return f(*args, **kwargs)
return decorated
def require_csrf(f):
@wraps(f)
def decorated(*args, **kwargs):
if request.method == 'POST':
token = request.form.get('csrf_token')
if not token or token != session.get('csrf_token'):
abort(403)
return f(*args, **kwargs)
return decorated
# Fixed: State-changing operations only via POST
@app.route('/account/close', methods=['POST']) # Not GET!
@require_auth
@require_csrf
def close_account():
# Fixed: Proper auth, CSRF protection, POST only
user_id = session['user_id'] # Use authenticated user, not parameter
db.execute("DELETE FROM accounts WHERE user_id = ?", [user_id])
return jsonify({"status": "closed"})
# Fixed: Transfer only via POST with proper validation
@app.route('/transfer', methods=['POST']) # Not GET!
@require_auth
@require_csrf
def transfer():
# Fixed: Verify from_account belongs to authenticated user
from_account = request.form.get('from')
if not user_owns_account(session['user_id'], from_account):
abort(403)
to_account = request.form.get('to')
amount = float(request.form.get('amount'))
# Fixed: Additional validation
if amount <= 0:
abort(400)
# Fixed: Use transaction
with db.transaction():
db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?",
[amount, from_account])
db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?",
[amount, to_account])
return jsonify({"status": "success"})
<?php
// Fixed: PHP with proper method and authorization checks
session_start();
// Fixed: Check authentication for ALL requests
function requireAuth() {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
die('Authentication required');
}
}
function requireAdmin() {
requireAuth();
if (!isset($_SESSION['is_admin']) || !$_SESSION['is_admin']) {
http_response_code(403);
die('Admin access required');
}
}
function verifyCSRF() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if ($token !== $_SESSION['csrf_token']) {
http_response_code(403);
die('Invalid CSRF token');
}
}
}
// Fixed: Only allow state-changing operations via POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
die('Method not allowed for this operation');
}
// Fixed: Verify auth and CSRF for all operations
requireAdmin();
verifyCSRF();
$action = $_POST['action'] ?? '';
switch ($action) {
case 'delete_user':
$userId = $_POST['user_id'];
// Additional authorization check
if (!canDeleteUser($_SESSION['user_id'], $userId)) {
http_response_code(403);
die('Not authorized to delete this user');
}
deleteUser($userId);
echo json_encode(['status' => 'deleted']);
break;
case 'reset_password':
$userId = $_POST['user_id'];
resetPassword($userId);
echo json_encode(['status' => 'reset']);
break;
default:
http_response_code(400);
die('Invalid action');
}
CVE Examples
No specific CVEs are commonly attributed to this CWE directly, though the pattern has contributed to numerous access control bypass vulnerabilities in web applications.
References
- MITRE Corporation. "CWE-650: Trusting HTTP Permission Methods on the Server Side." https://cwe.mitre.org/data/definitions/650.html
- OWASP. "REST Security Cheat Sheet."
- RFC 7231. "HTTP/1.1 Semantics and Content - Safe Methods."