Skip to content

Commit 44db72b

Browse files
committed
feat: add controller filter to check invalid chars in user input
1 parent 9a87de8 commit 44db72b

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

system/Filters/InvalidChars.php

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Filters;
13+
14+
use CodeIgniter\HTTP\RequestInterface;
15+
use CodeIgniter\HTTP\ResponseInterface;
16+
use RuntimeException;
17+
18+
/**
19+
* InvalidChars filter.
20+
*
21+
* Check if user input data ($_GET, $_POST, $_COOKIE, php://input) do not contain
22+
* invalid characters:
23+
* - invalid UTF-8 characters
24+
* - control characters except line break and tab code
25+
*/
26+
class InvalidChars implements FilterInterface
27+
{
28+
/**
29+
* Data source
30+
*
31+
* @var string
32+
*/
33+
protected $source;
34+
35+
/**
36+
* Check invalid characters.
37+
*
38+
* @param array|null $arguments
39+
*
40+
* @return void
41+
*/
42+
public function before(RequestInterface $request, $arguments = null)
43+
{
44+
if ($request->isCLI()) {
45+
return;
46+
}
47+
48+
$data = [
49+
'get' => $request->getGet(),
50+
'post' => $request->getPost(),
51+
'cookie' => $request->getCookie(),
52+
'rawInput' => $request->getRawInput(),
53+
];
54+
55+
foreach ($data as $source => $values) {
56+
$this->source = $source;
57+
$this->checkEncoding($values);
58+
$this->checkControl($values);
59+
}
60+
}
61+
62+
/**
63+
* We don't have anything to do here.
64+
*
65+
* @param array|null $arguments
66+
*
67+
* @return void
68+
*/
69+
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
70+
{
71+
}
72+
73+
/**
74+
* Check the character encoding is valid UTF-8.
75+
*
76+
* @param array|string $value
77+
*
78+
* @return array|string
79+
*/
80+
protected function checkEncoding($value)
81+
{
82+
if (is_array($value)) {
83+
array_map([$this, 'checkEncoding'], $value);
84+
85+
return $value;
86+
}
87+
88+
if (mb_check_encoding($value, 'UTF-8')) {
89+
return $value;
90+
}
91+
92+
throw new RuntimeException(
93+
'Invalid UTF-8 characters in ' . $this->source . ': ' . $value
94+
);
95+
}
96+
97+
/**
98+
* Check for the presence of control characters except line breaks and tabs.
99+
*
100+
* @param array|string $value
101+
*
102+
* @return array|string
103+
*/
104+
protected function checkControl($value)
105+
{
106+
if (is_array($value)) {
107+
array_map([$this, 'checkControl'], $value);
108+
109+
return $value;
110+
}
111+
112+
if (preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $value) === 1) {
113+
return $value;
114+
}
115+
116+
throw new RuntimeException(
117+
'Invalid Control characters in ' . $this->source . ': ' . $value
118+
);
119+
}
120+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Filters;
13+
14+
use CodeIgniter\HTTP\CLIRequest;
15+
use CodeIgniter\HTTP\IncomingRequest;
16+
use CodeIgniter\HTTP\URI;
17+
use CodeIgniter\HTTP\UserAgent;
18+
use CodeIgniter\Test\CIUnitTestCase;
19+
use CodeIgniter\Test\Mock\MockAppConfig;
20+
use RuntimeException;
21+
22+
/**
23+
* @internal
24+
*/
25+
final class InvalidCharsTest extends CIUnitTestCase
26+
{
27+
/**
28+
* @var InvalidChars
29+
*/
30+
private $invalidChars;
31+
32+
/**
33+
* @var IncomingRequest
34+
*/
35+
private $request;
36+
37+
protected function setUp(): void
38+
{
39+
parent::setUp();
40+
41+
$_GET = [];
42+
$_POST = [];
43+
$_COOKIE = [];
44+
45+
$this->request = $this->createRequest();
46+
$this->invalidChars = new InvalidChars();
47+
}
48+
49+
private function createRequest(): IncomingRequest
50+
{
51+
$config = new MockAppConfig();
52+
$uri = new URI();
53+
$userAgent = new UserAgent();
54+
$request = $this->getMockBuilder(IncomingRequest::class)
55+
->setConstructorArgs([$config, $uri, null, $userAgent])
56+
->onlyMethods(['isCLI'])
57+
->getMock();
58+
$request->method('isCLI')->willReturn(false);
59+
60+
return $request;
61+
}
62+
63+
public function testBeforeDoNothingWhenCLIRequest()
64+
{
65+
$cliRequest = new CLIRequest(new MockAppConfig());
66+
67+
$ret = $this->invalidChars->before($cliRequest);
68+
69+
$this->assertNull($ret);
70+
}
71+
72+
public function testBeforeValidString()
73+
{
74+
$_POST['val'] = [
75+
'valid string',
76+
];
77+
$_COOKIE['val'] = 'valid string';
78+
79+
$ret = $this->invalidChars->before($this->request);
80+
81+
$this->assertNull($ret);
82+
}
83+
84+
public function testBeforeInvalidUTF8StringCausesException()
85+
{
86+
$this->expectException(RuntimeException::class);
87+
$this->expectExceptionMessage('Invalid UTF-8 characters in post:');
88+
89+
$sjisString = mb_convert_encoding('SJISの文字列です。', 'SJIS');
90+
$_POST['val'] = [
91+
'valid string',
92+
$sjisString,
93+
];
94+
95+
$this->invalidChars->before($this->request);
96+
}
97+
98+
public function testBeforeInvalidControllCharCausesException()
99+
{
100+
$this->expectException(RuntimeException::class);
101+
$this->expectExceptionMessage('Invalid Control characters in cookie:');
102+
103+
$stringWithNullChar = "String contains null char and line break.\0\n";
104+
$_COOKIE['val'] = $stringWithNullChar;
105+
106+
$this->invalidChars->before($this->request);
107+
}
108+
109+
/**
110+
* @dataProvider stringWithLineBreakAndTabProvider
111+
*
112+
* @param string $input
113+
*/
114+
public function testCheckControlStringWithLineBreakAndTabReturnsTheString($input)
115+
{
116+
$_GET['val'] = $input;
117+
118+
$ret = $this->invalidChars->before($this->request);
119+
120+
$this->assertNull($ret);
121+
}
122+
123+
public function stringWithLineBreakAndTabProvider()
124+
{
125+
return [
126+
["String contains \n line break."],
127+
["String contains \r line break."],
128+
["String contains \r\n line break."],
129+
["String contains \t tab."],
130+
["String contains \t and \r line \n break."],
131+
];
132+
}
133+
134+
/**
135+
* @dataProvider stringWithControlCharsProvider
136+
*
137+
* @param string $input
138+
*/
139+
public function testCheckControlStringWithControlCharsCausesException($input)
140+
{
141+
$this->expectException(RuntimeException::class);
142+
$this->expectExceptionMessage('Invalid Control characters in get:');
143+
144+
$_GET['val'] = $input;
145+
146+
$ret = $this->invalidChars->before($this->request);
147+
148+
$this->assertNull($ret);
149+
}
150+
151+
public function stringWithControlCharsProvider()
152+
{
153+
return [
154+
["String contains null char.\0"],
155+
["String contains null char and line break.\0\n"],
156+
];
157+
}
158+
}

0 commit comments

Comments
 (0)