Skip to content

Commit cd23d73

Browse files
mpetrovichmarcj
authored andcommitted
Coerce offset and limit values to integers for MySQL LIMIT clause (#1464)
When constructing a MySQL LIMIT clause, values for the offset and limit are coerced to integers. This prevents arbitrary SQL from being injected via a query limit. Example: UserQuery::create()->limit('1;DROP TABLE users')->find(); Previously, this would have injected `DROP TABLE users` into the generated SQL. Now, the limit value would be coerced to the integer `1`. Fixes #1463
1 parent c64c0d6 commit cd23d73

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed

src/Propel/Runtime/Adapter/Pdo/MysqlAdapter.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ public function quoteIdentifierTable($table)
121121
*/
122122
public function applyLimit(&$sql, $offset, $limit)
123123
{
124+
$offset = (int) $offset;
125+
$limit = (int) $limit;
126+
124127
if ($limit >= 0) {
125128
$sql .= ' LIMIT ' . ($offset > 0 ? $offset . ', ' : '') . $limit;
126129
} elseif ($offset > 0) {
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<?php
2+
3+
namespace Propel\Tests\Issues;
4+
5+
use Propel\Runtime\Adapter\Pdo\MysqlAdapter;
6+
use Propel\Generator\Util\QuickBuilder;
7+
use Propel\Runtime\Propel;
8+
use Propel\Tests\TestCase;
9+
10+
11+
/**
12+
* Regression tests for a SQL injection vulnerability with `limit()`.
13+
*
14+
* @link https://github.com/propelorm/Propel2/issues/1463
15+
*/
16+
class Issue1463Test extends TestCase
17+
{
18+
19+
public function setUp()
20+
{
21+
parent::setUp();
22+
23+
if (class_exists('\Issue1463Item')) {
24+
return;
25+
}
26+
27+
$schema = <<<END
28+
<database name="issue_1463">
29+
<table name="issue_1463_item">
30+
<column name="id" type="INTEGER" size="10" sqlType="INT(10) UNSIGNED" primaryKey="true" required="true" autoIncrement="true" />
31+
<column name="name" type="VARCHAR" size="32" required="true" />
32+
</table>
33+
</database>
34+
END;
35+
$builder = new QuickBuilder();
36+
$builder->setSchema($schema);
37+
$builder->buildClasses(null, true);
38+
Propel::getServiceContainer()->setAdapter('issue_1463', new MysqlAdapter());
39+
}
40+
41+
/**
42+
* Verifies that the correct SQL is generated for queries that use `limit()`.
43+
*
44+
* @dataProvider dataLimit
45+
*/
46+
public function testLimit($limit, $expectedSql)
47+
{
48+
$query = \Issue1463ItemQuery::create()->limit($limit);
49+
50+
$params = [];
51+
$actualSql = $query->createSelectSql($params);
52+
53+
$this->assertEquals($expectedSql, $actualSql, 'Generated SQL does not match expected SQL');
54+
}
55+
56+
public function dataLimit()
57+
{
58+
return array(
59+
60+
/*
61+
Valid limits
62+
*/
63+
64+
'Zero' => array(
65+
'limit' => 0,
66+
'expectedSql' => 'SELECT FROM LIMIT 0'
67+
),
68+
69+
'Small integer' => array(
70+
'limit' => 38427,
71+
'expectedSql' => 'SELECT FROM LIMIT 38427'
72+
),
73+
'Small integer as a string' => array(
74+
'limit' => '38427',
75+
'expectedSql' => 'SELECT FROM LIMIT 38427'
76+
),
77+
78+
'Large integer' => array(
79+
'limit' => 9223372036854775807,
80+
'expectedSql' => 'SELECT FROM LIMIT 9223372036854775807'
81+
),
82+
'Large integer as a string' => array(
83+
'limit' => '9223372036854775807',
84+
'expectedSql' => 'SELECT FROM LIMIT 9223372036854775807'
85+
),
86+
87+
'Decimal value' => array(
88+
'limit' => 123.9,
89+
'expectedSql' => 'SELECT FROM LIMIT 123'
90+
),
91+
'Decimal value as a string' => array(
92+
'limit' => '123.9',
93+
'expectedSql' => 'SELECT FROM LIMIT 123'
94+
),
95+
96+
/*
97+
Invalid limits
98+
*/
99+
100+
'Negative value' => array(
101+
'limit' => -1,
102+
'expectedSql' => 'SELECT FROM '
103+
),
104+
'Non-numeric string' => array(
105+
'limit' => 'foo',
106+
'expectedSql' => 'SELECT FROM LIMIT 0'
107+
),
108+
'Injected SQL' => array(
109+
'limit' => '3;DROP TABLE abc',
110+
'expectedSql' => 'SELECT FROM LIMIT 3'
111+
),
112+
);
113+
}
114+
115+
/**
116+
* Verifies that the correct SQL is generated for queries that use `offset()`.
117+
*
118+
* @dataProvider dataOffset
119+
*/
120+
public function testOffset($offset, $expectedSql)
121+
{
122+
$query = \Issue1463ItemQuery::create()->offset($offset);
123+
124+
$params = [];
125+
$actualSql = $query->createSelectSql($params);
126+
127+
$this->assertEquals($expectedSql, $actualSql, 'Generated SQL does not match expected SQL');
128+
}
129+
130+
public function dataOffset()
131+
{
132+
return array(
133+
134+
/*
135+
Valid offsets
136+
*/
137+
138+
'Zero' => array(
139+
'offset' => 0,
140+
'expectedSql' => 'SELECT FROM '
141+
),
142+
143+
'Small integer' => array(
144+
'offset' => 38427,
145+
'expectedSql' => 'SELECT FROM LIMIT 38427, 18446744073709551615'
146+
),
147+
'Small integer as a string' => array(
148+
'offset' => '38427',
149+
'expectedSql' => 'SELECT FROM LIMIT 38427, 18446744073709551615'
150+
),
151+
152+
'Large integer' => array(
153+
'offset' => 9223372036854775807,
154+
'expectedSql' => 'SELECT FROM LIMIT 9223372036854775807, 18446744073709551615'
155+
),
156+
'Large integer as a string' => array(
157+
'offset' => '9223372036854775807',
158+
'expectedSql' => 'SELECT FROM LIMIT 9223372036854775807, 18446744073709551615'
159+
),
160+
161+
'Decimal value' => array(
162+
'offset' => 123.9,
163+
'expectedSql' => 'SELECT FROM LIMIT 123, 18446744073709551615'
164+
),
165+
'Decimal value as a string' => array(
166+
'offset' => '123.9',
167+
'expectedSql' => 'SELECT FROM LIMIT 123, 18446744073709551615'
168+
),
169+
170+
/*
171+
Invalid offsets
172+
*/
173+
174+
'Negative value' => array(
175+
'offset' => -1,
176+
'expectedSql' => 'SELECT FROM '
177+
),
178+
'Non-numeric string' => array(
179+
'offset' => 'foo',
180+
'expectedSql' => 'SELECT FROM '
181+
),
182+
'Injected SQL' => array(
183+
'offset' => '3;DROP TABLE abc',
184+
'expectedSql' => 'SELECT FROM LIMIT 3, 18446744073709551615'
185+
),
186+
);
187+
}
188+
189+
/**
190+
* Verifies that the correct SQL is generated for queries that use both `offset()` and `limit()`.
191+
*
192+
* @dataProvider dataOffsetAndLimit
193+
*/
194+
public function testOffsetAndLimit($offset, $expectedSql)
195+
{
196+
$query = \Issue1463ItemQuery::create()->offset($offset)->limit(999);
197+
198+
$params = [];
199+
$actualSql = $query->createSelectSql($params);
200+
201+
$this->assertEquals($expectedSql, $actualSql, 'Generated SQL does not match expected SQL');
202+
}
203+
204+
public function dataOffsetAndLimit()
205+
{
206+
return array(
207+
208+
/*
209+
Valid offsets
210+
*/
211+
212+
'Zero' => array(
213+
'offset' => 0,
214+
'expectedSql' => 'SELECT FROM LIMIT 999'
215+
),
216+
217+
'Small integer' => array(
218+
'offset' => 38427,
219+
'expectedSql' => 'SELECT FROM LIMIT 38427, 999'
220+
),
221+
'Small integer as a string' => array(
222+
'offset' => '38427',
223+
'expectedSql' => 'SELECT FROM LIMIT 38427, 999'
224+
),
225+
226+
'Large integer' => array(
227+
'offset' => 9223372036854775807,
228+
'expectedSql' => 'SELECT FROM LIMIT 9223372036854775807, 999'
229+
),
230+
'Large integer as a string' => array(
231+
'offset' => '9223372036854775807',
232+
'expectedSql' => 'SELECT FROM LIMIT 9223372036854775807, 999'
233+
),
234+
235+
'Decimal value' => array(
236+
'offset' => 123.9,
237+
'expectedSql' => 'SELECT FROM LIMIT 123, 999'
238+
),
239+
'Decimal value as a string' => array(
240+
'offset' => '123.9',
241+
'expectedSql' => 'SELECT FROM LIMIT 123, 999'
242+
),
243+
244+
/*
245+
Invalid offsets
246+
*/
247+
248+
'Negative value' => array(
249+
'offset' => -1,
250+
'expectedSql' => 'SELECT FROM LIMIT 999'
251+
),
252+
'Non-numeric string' => array(
253+
'offset' => 'foo',
254+
'expectedSql' => 'SELECT FROM LIMIT 999'
255+
),
256+
'Injected SQL' => array(
257+
'offset' => '3;DROP TABLE abc',
258+
'expectedSql' => 'SELECT FROM LIMIT 3, 999'
259+
),
260+
);
261+
}
262+
263+
}

0 commit comments

Comments
 (0)