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

ImpactDetails
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Attackers can bypass method-based access controls to perform privileged operations.
IntegrityScope: Integrity

Modify Application Data - State-changing operations via GET allow unauthorized data modification.
ConfidentialityScope: 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

  1. MITRE Corporation. "CWE-650: Trusting HTTP Permission Methods on the Server Side." https://cwe.mitre.org/data/definitions/650.html
  2. OWASP. "REST Security Cheat Sheet."
  3. RFC 7231. "HTTP/1.1 Semantics and Content - Safe Methods."