4

I have a small Rust program that formats MySQL queries, but I found it failing on bigger queries, returning

Urlencoded payload size is bigger than allowed (default: 256kB)

I'm using actix web, which looks like this

use actix_web::{
    web, App, HttpResponse, HttpServer, Result,
};

#[derive(Deserialize)]
pub struct MyParams {
    q: String,
}

fn index(params: web::Form<MyParams>) -> Result<HttpResponse> {
    Ok(HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body("test"))
        // .body(mysql_format2(&params.q[..])))
}

fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            web::resource("/")
                .route(web::post().to(index))
        )
    })
    .bind("127.0.0.1:48627")?
    .run()
}

And the PHP script calling this looks like

$this->FormattedMySQL = str_repeat("make a big query ", 1024*256);

$query =  http_build_query([
    'q' => $this->FormattedMySQL,
]);

// if I change this to 16385 (+1)
// then it breaks and returns that error
$query = substr($query, 0, 16384);

if ($this->FormatMySQL && strlen($query) <= 262144) {
    try {
        $this->VerbosePrint('formatting');
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, MYSQL_FORMAT_ENDPOINT);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        $this->FormattedMySQL = curl_exec($ch);
        curl_close($ch);
    } catch (Exception $_) { }
}

What am I missing, or why does this seem to be throwing this error for 16kB instead of 256kB?

I see that it's also possible to set the Payload config but I'm not totally sure how/where to apply this in my existing code.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Brian Leishman
  • 8,155
  • 11
  • 57
  • 93

1 Answers1

3

The quick answer is that there is a second config (FormConfig), which is what you're hitting your head against. It is unclear as it returns the same error type as PayloadConfig (there is a reason for that, which I will explain in the "longer version")

Change your actix server definition to this to change it to, say, 256kb:

HttpServer::new(|| {
    App::new().service(
        web::resource("/")
            .route(web::post()
                .to(index)
            )
            .data(web::Form::<MyParams>::configure(|cfg| cfg.limit(256 * 1024)))
    )
})
.bind("127.0.0.1:48627")?
.run()

You will also need to import actix_web::FromRequest, as this is where the object type change from a request to a urlencoded form happens and where the configure() method lives.

Now, for the explanation!

Actix, much like other frameworks, has multiple layers of limits in place. You've found one of them, this is another.

The one you found prevents denial of service through memory exhaustion (i.e. somebody sending a deliberately large payload to oom your server, as it will have to store the body somewhere to process). It governs the entire payload of the request.

The one you are hitting against is a much smaller limitation placed on each individual field, and it exists to prevent a different kind of exhaustion attack. Suppose your attacker knows you're parsing the input with something; let's pretend it's JSON. By sending an arbitrarily large and convoluted JSON, they can effectively lock your server down while processing a single request. Small data input, very large consequences.

For that reason, the two limits are typically independent of each other, and allow you to fine-tune the restrictions based on your requirements. Lots of small fields? No problem. One huge chunk? Also not a problem.

The effect of the FormConfig limit is located here in case you want to see the code itself. As the type return is the same as that of the PayloadConfig limit ( (the Overflow variant of this struct)[https://docs.rs/actix-web/1.0.7/actix_web/error/enum.UrlencodedError.html]), the message is unclear and you end up scratching your head as a result.

The main aim of the error being "similar" was, I think, to prevent the server from indicating to a potential attacker which limit they hit, and it seems to be what they had in mind. The user-facing error description is the problem, and that's something that may get adjusted with a well-placed PR.

Sébastien Renauld
  • 19,203
  • 2
  • 46
  • 66