]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
1 | <?php |
2 | ||
3 | /** | |
4 | * This class is adapted from code coming from Zend Framework. | |
5 | * | |
6 | * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) | |
7 | * @license http://framework.zend.com/license/new-bsd New BSD License | |
8 | */ | |
9 | ||
10 | class Twig_Test_EscapingTest extends PHPUnit_Framework_TestCase | |
11 | { | |
12 | /** | |
13 | * All character encodings supported by htmlspecialchars() | |
14 | */ | |
15 | protected $htmlSpecialChars = array( | |
16 | '\'' => ''', | |
17 | '"' => '"', | |
18 | '<' => '<', | |
19 | '>' => '>', | |
20 | '&' => '&' | |
21 | ); | |
22 | ||
23 | protected $htmlAttrSpecialChars = array( | |
24 | '\'' => ''', | |
25 | /* Characters beyond ASCII value 255 to unicode escape */ | |
26 | 'Ā' => 'Ā', | |
27 | /* Immune chars excluded */ | |
28 | ',' => ',', | |
29 | '.' => '.', | |
30 | '-' => '-', | |
31 | '_' => '_', | |
32 | /* Basic alnums excluded */ | |
33 | 'a' => 'a', | |
34 | 'A' => 'A', | |
35 | 'z' => 'z', | |
36 | 'Z' => 'Z', | |
37 | '0' => '0', | |
38 | '9' => '9', | |
39 | /* Basic control characters and null */ | |
40 | "\r" => '
', | |
41 | "\n" => '
', | |
42 | "\t" => '	', | |
43 | "\0" => '�', // should use Unicode replacement char | |
44 | /* Encode chars as named entities where possible */ | |
45 | '<' => '<', | |
46 | '>' => '>', | |
47 | '&' => '&', | |
48 | '"' => '"', | |
49 | /* Encode spaces for quoteless attribute protection */ | |
50 | ' ' => ' ', | |
51 | ); | |
52 | ||
53 | protected $jsSpecialChars = array( | |
54 | /* HTML special chars - escape without exception to hex */ | |
55 | '<' => '\\x3C', | |
56 | '>' => '\\x3E', | |
57 | '\'' => '\\x27', | |
58 | '"' => '\\x22', | |
59 | '&' => '\\x26', | |
60 | /* Characters beyond ASCII value 255 to unicode escape */ | |
61 | 'Ā' => '\\u0100', | |
62 | /* Immune chars excluded */ | |
63 | ',' => ',', | |
64 | '.' => '.', | |
65 | '_' => '_', | |
66 | /* Basic alnums excluded */ | |
67 | 'a' => 'a', | |
68 | 'A' => 'A', | |
69 | 'z' => 'z', | |
70 | 'Z' => 'Z', | |
71 | '0' => '0', | |
72 | '9' => '9', | |
73 | /* Basic control characters and null */ | |
74 | "\r" => '\\x0D', | |
75 | "\n" => '\\x0A', | |
76 | "\t" => '\\x09', | |
77 | "\0" => '\\x00', | |
78 | /* Encode spaces for quoteless attribute protection */ | |
79 | ' ' => '\\x20', | |
80 | ); | |
81 | ||
82 | protected $urlSpecialChars = array( | |
83 | /* HTML special chars - escape without exception to percent encoding */ | |
84 | '<' => '%3C', | |
85 | '>' => '%3E', | |
86 | '\'' => '%27', | |
87 | '"' => '%22', | |
88 | '&' => '%26', | |
89 | /* Characters beyond ASCII value 255 to hex sequence */ | |
90 | 'Ā' => '%C4%80', | |
91 | /* Punctuation and unreserved check */ | |
92 | ',' => '%2C', | |
93 | '.' => '.', | |
94 | '_' => '_', | |
95 | '-' => '-', | |
96 | ':' => '%3A', | |
97 | ';' => '%3B', | |
98 | '!' => '%21', | |
99 | /* Basic alnums excluded */ | |
100 | 'a' => 'a', | |
101 | 'A' => 'A', | |
102 | 'z' => 'z', | |
103 | 'Z' => 'Z', | |
104 | '0' => '0', | |
105 | '9' => '9', | |
106 | /* Basic control characters and null */ | |
107 | "\r" => '%0D', | |
108 | "\n" => '%0A', | |
109 | "\t" => '%09', | |
110 | "\0" => '%00', | |
111 | /* PHP quirks from the past */ | |
112 | ' ' => '%20', | |
113 | '~' => '~', | |
114 | '+' => '%2B', | |
115 | ); | |
116 | ||
117 | protected $cssSpecialChars = array( | |
118 | /* HTML special chars - escape without exception to hex */ | |
119 | '<' => '\\3C ', | |
120 | '>' => '\\3E ', | |
121 | '\'' => '\\27 ', | |
122 | '"' => '\\22 ', | |
123 | '&' => '\\26 ', | |
124 | /* Characters beyond ASCII value 255 to unicode escape */ | |
125 | 'Ā' => '\\100 ', | |
126 | /* Immune chars excluded */ | |
127 | ',' => '\\2C ', | |
128 | '.' => '\\2E ', | |
129 | '_' => '\\5F ', | |
130 | /* Basic alnums excluded */ | |
131 | 'a' => 'a', | |
132 | 'A' => 'A', | |
133 | 'z' => 'z', | |
134 | 'Z' => 'Z', | |
135 | '0' => '0', | |
136 | '9' => '9', | |
137 | /* Basic control characters and null */ | |
138 | "\r" => '\\D ', | |
139 | "\n" => '\\A ', | |
140 | "\t" => '\\9 ', | |
141 | "\0" => '\\0 ', | |
142 | /* Encode spaces for quoteless attribute protection */ | |
143 | ' ' => '\\20 ', | |
144 | ); | |
145 | ||
146 | protected $env; | |
147 | ||
148 | public function setUp() | |
149 | { | |
150 | $this->env = new Twig_Environment(); | |
151 | } | |
152 | ||
153 | public function testHtmlEscapingConvertsSpecialChars() | |
154 | { | |
155 | foreach ($this->htmlSpecialChars as $key => $value) { | |
156 | $this->assertEquals($value, twig_escape_filter($this->env, $key, 'html'), 'Failed to escape: '.$key); | |
157 | } | |
158 | } | |
159 | ||
160 | public function testHtmlAttributeEscapingConvertsSpecialChars() | |
161 | { | |
162 | foreach ($this->htmlAttrSpecialChars as $key => $value) { | |
163 | $this->assertEquals($value, twig_escape_filter($this->env, $key, 'html_attr'), 'Failed to escape: '.$key); | |
164 | } | |
165 | } | |
166 | ||
167 | public function testJavascriptEscapingConvertsSpecialChars() | |
168 | { | |
169 | foreach ($this->jsSpecialChars as $key => $value) { | |
170 | $this->assertEquals($value, twig_escape_filter($this->env, $key, 'js'), 'Failed to escape: '.$key); | |
171 | } | |
172 | } | |
173 | ||
174 | public function testJavascriptEscapingReturnsStringIfZeroLength() | |
175 | { | |
176 | $this->assertEquals('', twig_escape_filter($this->env, '', 'js')); | |
177 | } | |
178 | ||
179 | public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits() | |
180 | { | |
181 | $this->assertEquals('123', twig_escape_filter($this->env, '123', 'js')); | |
182 | } | |
183 | ||
184 | public function testCssEscapingConvertsSpecialChars() | |
185 | { | |
186 | foreach ($this->cssSpecialChars as $key => $value) { | |
187 | $this->assertEquals($value, twig_escape_filter($this->env, $key, 'css'), 'Failed to escape: '.$key); | |
188 | } | |
189 | } | |
190 | ||
191 | public function testCssEscapingReturnsStringIfZeroLength() | |
192 | { | |
193 | $this->assertEquals('', twig_escape_filter($this->env, '', 'css')); | |
194 | } | |
195 | ||
196 | public function testCssEscapingReturnsStringIfContainsOnlyDigits() | |
197 | { | |
198 | $this->assertEquals('123', twig_escape_filter($this->env, '123', 'css')); | |
199 | } | |
200 | ||
201 | public function testUrlEscapingConvertsSpecialChars() | |
202 | { | |
203 | foreach ($this->urlSpecialChars as $key => $value) { | |
204 | $this->assertEquals($value, twig_escape_filter($this->env, $key, 'url'), 'Failed to escape: '.$key); | |
205 | } | |
206 | } | |
207 | ||
208 | /** | |
209 | * Range tests to confirm escaped range of characters is within OWASP recommendation | |
210 | */ | |
211 | ||
212 | /** | |
213 | * Only testing the first few 2 ranges on this prot. function as that's all these | |
214 | * other range tests require | |
215 | */ | |
216 | public function testUnicodeCodepointConversionToUtf8() | |
217 | { | |
218 | $expected = " ~ޙ"; | |
219 | $codepoints = array(0x20, 0x7e, 0x799); | |
220 | $result = ''; | |
221 | foreach ($codepoints as $value) { | |
222 | $result .= $this->codepointToUtf8($value); | |
223 | } | |
224 | $this->assertEquals($expected, $result); | |
225 | } | |
226 | ||
227 | /** | |
228 | * Convert a Unicode Codepoint to a literal UTF-8 character. | |
229 | * | |
230 | * @param int Unicode codepoint in hex notation | |
231 | * @return string UTF-8 literal string | |
232 | */ | |
233 | protected function codepointToUtf8($codepoint) | |
234 | { | |
235 | if ($codepoint < 0x80) { | |
236 | return chr($codepoint); | |
237 | } | |
238 | if ($codepoint < 0x800) { | |
239 | return chr($codepoint >> 6 & 0x3f | 0xc0) | |
240 | . chr($codepoint & 0x3f | 0x80); | |
241 | } | |
242 | if ($codepoint < 0x10000) { | |
243 | return chr($codepoint >> 12 & 0x0f | 0xe0) | |
244 | . chr($codepoint >> 6 & 0x3f | 0x80) | |
245 | . chr($codepoint & 0x3f | 0x80); | |
246 | } | |
247 | if ($codepoint < 0x110000) { | |
248 | return chr($codepoint >> 18 & 0x07 | 0xf0) | |
249 | . chr($codepoint >> 12 & 0x3f | 0x80) | |
250 | . chr($codepoint >> 6 & 0x3f | 0x80) | |
251 | . chr($codepoint & 0x3f | 0x80); | |
252 | } | |
253 | throw new Exception('Codepoint requested outside of Unicode range'); | |
254 | } | |
255 | ||
256 | public function testJavascriptEscapingEscapesOwaspRecommendedRanges() | |
257 | { | |
258 | $immune = array(',', '.', '_'); // Exceptions to escaping ranges | |
259 | for ($chr=0; $chr < 0xFF; $chr++) { | |
260 | if ($chr >= 0x30 && $chr <= 0x39 | |
261 | || $chr >= 0x41 && $chr <= 0x5A | |
262 | || $chr >= 0x61 && $chr <= 0x7A) { | |
263 | $literal = $this->codepointToUtf8($chr); | |
264 | $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js')); | |
265 | } else { | |
266 | $literal = $this->codepointToUtf8($chr); | |
267 | if (in_array($literal, $immune)) { | |
268 | $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js')); | |
269 | } else { | |
270 | $this->assertNotEquals( | |
271 | $literal, | |
272 | twig_escape_filter($this->env, $literal, 'js'), | |
273 | "$literal should be escaped!"); | |
274 | } | |
275 | } | |
276 | } | |
277 | } | |
278 | ||
279 | public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges() | |
280 | { | |
281 | $immune = array(',', '.', '-', '_'); // Exceptions to escaping ranges | |
282 | for ($chr=0; $chr < 0xFF; $chr++) { | |
283 | if ($chr >= 0x30 && $chr <= 0x39 | |
284 | || $chr >= 0x41 && $chr <= 0x5A | |
285 | || $chr >= 0x61 && $chr <= 0x7A) { | |
286 | $literal = $this->codepointToUtf8($chr); | |
287 | $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr')); | |
288 | } else { | |
289 | $literal = $this->codepointToUtf8($chr); | |
290 | if (in_array($literal, $immune)) { | |
291 | $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr')); | |
292 | } else { | |
293 | $this->assertNotEquals( | |
294 | $literal, | |
295 | twig_escape_filter($this->env, $literal, 'html_attr'), | |
296 | "$literal should be escaped!"); | |
297 | } | |
298 | } | |
299 | } | |
300 | } | |
301 | ||
302 | public function testCssEscapingEscapesOwaspRecommendedRanges() | |
303 | { | |
304 | $immune = array(); // CSS has no exceptions to escaping ranges | |
305 | for ($chr=0; $chr < 0xFF; $chr++) { | |
306 | if ($chr >= 0x30 && $chr <= 0x39 | |
307 | || $chr >= 0x41 && $chr <= 0x5A | |
308 | || $chr >= 0x61 && $chr <= 0x7A) { | |
309 | $literal = $this->codepointToUtf8($chr); | |
310 | $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'css')); | |
311 | } else { | |
312 | $literal = $this->codepointToUtf8($chr); | |
313 | $this->assertNotEquals( | |
314 | $literal, | |
315 | twig_escape_filter($this->env, $literal, 'css'), | |
316 | "$literal should be escaped!"); | |
317 | } | |
318 | } | |
319 | } | |
320 | } |