Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Eyad Mohammed Osama's avatar

Is QueryBuilder's paginate() method vulnerable to SQL injection ?

Recently, I've been working on a project that involves fetching data from a database using QueryBuilder class using the following code:

	$page = $request->page;
    $perpage = $request->perpage;

    $builder = DB::table('users');
    $builder->orderByDesc("id");
    $builder->whereNull("deleted_at");
    if ($request->has("keyword")) {
        $search = $request->keyword;
        $builder->where(function ($builder) use ($search) {
            $builder->where("id", $search);
            $builder->orWhere("fullname", "LIKE", "%" . $search . "%");
            $builder->orWhere("email", "LIKE", "%" . $search . "%");
            $builder->orWhere("mobile_number", "LIKE", "%" . $search . "%");
        });
    }
    $users = $builder->paginate(page: $page, perPage: $perpage);

However, during the penetration testing, the report says that perPage parameter is vulnerable to SQL injection vulnerability

Can this really happen here or Laravel would escape the values ?

Thanks in advance

0 likes
28 replies
tykus's avatar

I don't know how it would be - the perPage value is cast as an int in the limit operation; and mathematical operation in take operation will not pass thru non-numeric values.

3 likes
tisuchi's avatar

@eyad mohammed osama

For double protection, you can sanitize requested data.


$page = $request->page;
$perpage = $request->perpage;
1 like
Sinnbeck's avatar

If it was the case, then every single site using laravel and pagination would be vulnerable. It seems unlikely that they are the first in the world to find a vulnerability there. Any chance they said how it could be exploited?

2 likes
jlrdw's avatar

The pagination (laravel paginate()) as asked in question has nothing to do with a query. That is where binding parameters comes in.

Pagination (laravel paginate()) it's just for the links to next and previous.

But if using any raw statements sanitize your input query string parameters.

Go to the owasp site and study up on security.

tykus's avatar

@jlrdw perPage is used in the query for the offset and limit 🤷‍♂️

tykus's avatar

@jlrdw there is an echo in here

But perpage is int. It should error out if some "bogus" SQL injection is passed instead.

the perPage value is cast as an int in the limit operation; and mathematical operation in take operation will not pass thru non-numeric values.

jlrdw's avatar

I know, I don't understand the reply where you told me about perpage? I tested, and sure enough passing non numeric gives:

TypeError PHP 8.1.1 9.6.0 Unsupported operand types: string * int

If anything I agree.

Edit

Sorry if I misunderstood something.

Snapey's avatar

@jlrdw probably your comment

The pagination has nothing to do with a query

when clearly it does

Eyad Mohammed Osama's avatar

@jlrdw If we try to substitute the string 15%00%C0%A7%C0%A2%252527%252522 in perpage parameter, it actually throws the following error:

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '' at line 1

select * from users where deleted_at is null order by id desc offset 0

And the generated query seems to be missing the LIMIT clause

tykus's avatar

@Eyad Mohammed Osama I don't understand how that should fall all the way through to the database - the mathematical operation should fail in PHP.

What are your Laravel and PHP versions?

Snapey's avatar

I don't understand your use of pagination() ?

 $users = $builder->paginate(page: $page, perPage: $perpage);

The first parameter should be the number per page. Optional parameters are column and page name ?

Eyad Mohammed Osama's avatar

@Snapey Thanks for the reply

In fact, i have a table that views users, and there's an option to control how many users should be returned per page

I'm reading the parameters from the request and passing them to the paginate() method

Since i prefer readability and cannot memorize parameters order, i use PHP 8 named arguments to pass the arguments

jlrdw's avatar

@Eyad Mohammed Osama if you use the regular paginator or length aware paginator per documentation and API then:

No pagination (laravel paginate()) is not vulnerable to SQL injection.

Edit:

Why not try yourself to inject something?

1 like
jlrdw's avatar

@Eyad Mohammed Osama

Regular = just using paginate() or simplepaginate().

Otherwise you have the lengthawarepaginator where you can customize.

All is in documentation and API.

Edit, where is

$builder->paginate(page: $page, perPage: $perpage)

in documentation? Where are you calling:

new LengthAwarePaginator
1 like
jlrdw's avatar

@Eyad Mohammed Osama then this:

$users = $builder->paginate(page: $page, perPage: $perpage);

can be:

$builder->paginate($perpage);

It is safe as said by many above.

1 like
Eyad Mohammed Osama's avatar

@jlrdw

Excuse me for over asking, but how it would be safe if i don't pass the page parameter ?

Even if i don't pass the page parameter, Laravel will automatically detect it and inserts it into the generated URL, so it happens implicitly

jlrdw's avatar

@Eyad Mohammed Osama it's resolved in the paginators code, but if you feel safer pass it as well yourself.

1 like
Sinnbeck's avatar

@Eyad Mohammed Osama I just tested locally and $perpage is just null, and causes the error you mentioned.

Try dd($perpage) and you should just see null

Adding a fallback fixes it right away :)

$perpage = $request->perpage ?? 15;

Or you can use validation

$validator = Validator::make($request->all(), [
        'perpage' => [\Illuminate\Validation\Rule::in([10,15,20])]
    ]);
    if ($validator->fails()) {
        return redirect()->back(); //replace with whatever you want
    }

Did they report it as they managed to get it to throw an error perhaps?

1 like
Eyad Mohammed Osama's avatar

@Sinnbeck Thanks for your effort and time I applied your solution because the best practice is to sanitize input and make sure it's a positive integer

Please or to participate in this conversation.