OLD | NEW |
---|---|
(Empty) | |
1 # Copyright 2017 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 # | |
5 # | |
6 # This test checks that Chrome does not crash or otherwise wildly misbehave when | |
7 # it receives unexpected Chrome-Proxy header values and/or directives. This is | |
8 # done by fuzz testing which is configured through a dictionary object at the | |
9 # top of this file. | |
10 # | |
11 # The fuzz testing generates URLs which are requested from the Chrome Proxy test | |
12 # server which will modify its response based on the encoded data in the URL. | |
13 # | |
14 # The fuzz testing is configured in a dictionary-type object of the following | |
15 # format: | |
16 # { | |
17 # # This specifies a header that will be fuzzed. Only one header is fuzzed | |
18 # # per test. When a header is being fuzz tested, any entry it has in the | |
19 # # STATIC_RESPONSE_HEADERS object will be overwritten. | |
20 # "chrome-proxy": { | |
21 # # This specifies the directive key values that are possible to use in | |
22 # # fuzzing. | |
23 # "directive_keys: [ | |
24 # "page-policy", | |
25 # "foo", | |
26 # ], | |
27 # # This specifies the directive values that are possible to use in | |
28 # # fuzzing. Any one of these values may end up mapped to any one of the | |
29 # # directive_keys above. | |
30 # "directive_values": [ | |
31 # "empty-image", | |
32 # "bar", | |
33 # ], | |
34 # # The maximum number of directives to use in this header. If this value | |
35 # # is greater than the number of directives given above, that will be | |
36 # # used instead. | |
37 # "max_directives": 10, | |
38 # | |
39 # # The maximum number of directive values to use per directive. If this | |
40 # # value is greater than the number of directive values given above, that | |
41 # # will be used instead. | |
42 # "max_directive_values": 10, | |
43 # | |
44 # # This is used to join the directive values when multiple are used. | |
45 # "directive_value_joiner": "|", | |
46 # }, | |
47 # } | |
48 # | |
49 # If the above configuration was given, the following values would be generated | |
50 # for the chrome-proxy header: | |
51 # <empty-string> | |
52 # foo | |
53 # foo=bar | |
54 # foo=empty-image | |
55 # foo=bar|empty-image | |
56 # page_policies | |
57 # page_policies=bar | |
58 # page_policies=empty-image | |
59 # page_policies=bar|empty-image | |
60 # foo page_policies | |
61 # foo=bar page_policies=bar | |
62 # foo=empty-image page_policies=empty-image | |
63 # foo=bar|empty-image page_policies=bar|empty-image | |
Tom Bergan
2017/06/07 22:18:08
Directives should be comma-separated, I think?
Robert Ogden
2017/06/07 23:38:35
Done.
| |
64 # | |
65 # Randomly generated values are also supported in the fuzz_header field | |
66 # ("chrome-proxy" in the above example), directive_keys, and directive_values. | |
67 # Randomly generated values can be specified using these formats: | |
68 # {{RAND_STR(N)}} | |
69 # Creates a random string of lowercase and digit characters of length N | |
70 # | |
71 # {{RAND_INT(N)}} | |
72 # Creates a random integer in the range [0, 10^N+1) with leading zeros | |
73 # Example: "Num cookies: {{RAND_INT(4)}}" yields "Num cookies: 0123" | |
74 # | |
75 # {{RAND_DBL(P.Q)}} | |
76 # Creates a random double in the range [0, 10^P+1) with leading zeros and | |
77 # up to Q places after the decimal point | |
78 # Example: "My money = ${{RAND_DBL(3,2)}}" yields "My money = $001.53" | |
79 # | |
80 # {{RAND_BOOL}} | |
81 # Creates a random boolean, either 'true' or 'false' | |
82 # Example: "I am awesome: {{RAND_BOOL}}" yields "I am awesome: true" | |
83 | |
84 import BaseHTTPServer | |
85 import base64 | |
86 import itertools | |
87 import json | |
88 import random | |
89 import re | |
90 import string | |
91 | |
92 from common import TestDriver | |
93 from common import IntegrationTest | |
94 from decorators import Slow | |
95 | |
96 # This dict configures how the fuzzing will operate. See documentation above for | |
97 # more information. | |
98 FUZZ_HEADERS = { | |
99 "chrome-proxy": { | |
100 "directive_keys": [ | |
101 "foo", | |
102 "{{RAND_STR(10)}}", | |
103 "page-policies", | |
104 "q", | |
105 "block-once", | |
106 "block", | |
107 "bypass", | |
108 ], | |
109 "directive_values": [ | |
110 "bar", | |
111 "{{RAND_INT(1)}}", | |
112 "{{RAND_INT(4)}}", | |
113 "empty-image", | |
114 "{{RAND_STR(10)}}", | |
115 "low", | |
116 "preview", | |
117 ], | |
118 "max_directives": 10, | |
Tom Bergan
2017/06/07 22:18:08
You can probably shrink this to 3, since most bugs
Robert Ogden
2017/06/07 23:38:36
Done.
| |
119 "max_directive_values": 10, | |
Tom Bergan
2017/06/07 22:18:08
Ditto here.
Robert Ogden
2017/06/07 23:38:35
Done.
| |
120 "directive_value_joiner": "|", | |
121 }, | |
122 } | |
123 | |
124 TEST_SERVER = "chromeproxy-test.appspot.com" | |
125 | |
126 # These headers will be present in every test server response. If one of these | |
127 # entries is also a fuzzed header above, then the fuzzed value will take the | |
128 # place of the static one instead. | |
129 STATIC_RESPONSE_HEADERS = { | |
130 "content-type": ["text/html"], | |
131 "via": ["1.1 Chrome-Compression-Proxy"], | |
132 "cache-control": ["no-cache, no-store, must-revalidate"], | |
133 "pragma": ["no-cache"], | |
134 "expires": ["0"], | |
135 } | |
136 | |
137 # This string will be used as the response body in every test and will be | |
138 # checked for existance on the final loaded page. | |
139 STATIC_RESPONSE_BODY = 'ok' | |
140 | |
141 rand_str_re = re.compile(r'{{RAND_STR\((\d+)\)}}') | |
142 rand_int_re = re.compile(r'{{RAND_INT\((\d+)\)}}') | |
143 rand_dbl_re = re.compile(r'{{RAND_DBL\((\d+)\.(\d+)\)}}') | |
144 rand_bool_re = re.compile(r'{{RAND_BOOL}}') | |
145 | |
146 def ParseRand(key, val): | |
147 """This helper function parses the {{RAND}} expressions in the given values | |
148 and returns them with random values subsituted in place. | |
149 | |
150 Args: | |
151 key: the header key with 0 or more {{RAND}} expressions | |
152 val: the header value with 0 or more {{RAND}} expressions | |
153 Returns: | |
154 A key, value tuple with subsituted random values | |
155 """ | |
156 def GenerateRand(length, charset): | |
157 return ''.join(random.choice(charset) for _ in range(length)) | |
158 def _parse_rand(v): | |
159 result = v | |
160 had_match = True | |
161 while had_match: | |
162 had_match = False | |
163 str_match = rand_str_re.search(result) | |
164 if str_match: | |
165 had_match = True | |
166 mag = int(result[str_match.start(1):str_match.end(1)]) | |
167 rand_str = GenerateRand(mag, string.ascii_lowercase + string.digits) | |
168 result = (result[:str_match.start()] + rand_str | |
169 + result[str_match.end():]) | |
170 int_match = rand_int_re.search(result) | |
171 if int_match: | |
172 had_match = True | |
173 mag = int(result[int_match.start(1):int_match.end(1)]) | |
174 rand_int = GenerateRand(mag, string.digits) | |
175 result = (result[:int_match.start()] + rand_int | |
176 + result[int_match.end():]) | |
177 dbl_match = rand_dbl_re.search(result) | |
178 if dbl_match: | |
179 had_match = True | |
180 magN = int(result[dbl_match.start(1):dbl_match.end(1)]) | |
181 magD = int(result[dbl_match.start(2):dbl_match.end(2)]) | |
182 rand_dbl = GenerateRand(magN, string.digits) + '.' + GenerateRand(magD, | |
183 string.digits) | |
184 result = (result[:dbl_match.start()] + rand_dbl | |
185 + result[dbl_match.end():]) | |
186 bool_match = rand_bool_re.search(result) | |
187 if bool_match: | |
188 had_match = True | |
189 rand_bool = bool(random.getrandbits(1)) | |
190 result = (result[:bool_match.start()] + str(rand_bool).lower() | |
191 + result[bool_match.end():]) | |
192 return result | |
193 return (_parse_rand(key), _parse_rand(val)) | |
194 | |
195 def GenerateFuzzedHeaders(cfg=FUZZ_HEADERS): | |
196 """This function yields header key value pairs which can be used to update a | |
197 Python dict representing HTTP headers. See file level documentation for more | |
198 information. | |
199 | |
200 Args: | |
201 cfg: the configuration dict that specifies how to fuzz the proxy headers | |
202 Yields: | |
203 one header key value pair | |
204 """ | |
205 for header_key in cfg: | |
206 fuzz = cfg[header_key] | |
207 dirs = fuzz['directive_keys'] | |
208 vals = fuzz['directive_values'] | |
209 max_dirs = min(fuzz['max_directives'], len(dirs)) | |
210 max_vals = min(fuzz['max_directive_values'], len(vals)) | |
211 def GenerateFuzzedValues(): | |
212 for n in range(0, max_vals + 1): | |
213 for c in itertools.combinations(vals, n): | |
214 yield c | |
215 # Yield an empty header key,value pair before doing all the combinations. | |
216 yield (header_key, '') | |
217 for num_dirs in range(1, max_dirs + 1): | |
218 for directive_set in itertools.combinations(dirs, num_dirs): | |
219 for values in GenerateFuzzedValues(): | |
220 value_list = list(values) | |
221 if '' in value_list: | |
222 value_list.remove('') | |
223 value_str = fuzz['directive_value_joiner'].join(value_list) | |
224 header = [] | |
225 for directive in directive_set: | |
226 if len(value_str) == 0: | |
227 header.append(directive) | |
228 else: | |
229 header.append('%s=%s' % (directive, value_str)) | |
230 yield ParseRand(header_key, ' '.join(header)) | |
231 | |
232 class FuzzUnitTests(IntegrationTest): | |
233 | |
234 def testParseRand(self): | |
235 tests = { | |
236 "{{RAND_STR(1)}": r"{{RAND_STR\(1\)}", | |
237 "{{RAND_INT(1)}": r"{{RAND_INT\(1\)}", | |
238 "{{RAND_DBL(1.1)}": r"{{RAND_DBL\(1\.1\)}", | |
239 "{{RAND_DBL(11)}}": r"{{RAND_DBL\(11\)}}", | |
240 "{{RAND_BOOL}": r"{{RAND_BOOL}", | |
241 "{{RAND_STR(0)}}": "", | |
242 "hi{{RAND_STR(0)}}": "hi", | |
243 "{{RAND_STR(0)}}there": "there", | |
244 "hi{{RAND_STR(0)}}there": "hithere", | |
245 "{{RAND_STR(3)}}": r"[a-z0-9][a-z0-9][a-z0-9]", | |
246 "{{RAND_STR(3)}}there": r"[a-z0-9][a-z0-9][a-z0-9]there", | |
247 "hi{{RAND_STR(3)}}": r"hi[a-z0-9][a-z0-9][a-z0-9]", | |
248 "hi{{RAND_STR(3)}}there": r"hi[a-z0-9][a-z0-9][a-z0-9]there", | |
249 "{{RAND_INT(0)}}": "", | |
250 "hi{{RAND_INT(0)}}": "hi", | |
251 "{{RAND_INT(0)}}there": "there", | |
252 "hi{{RAND_INT(0)}}there": "hithere", | |
253 "{{RAND_INT(3)}}": r"[0-9][0-9][0-9]", | |
254 "{{RAND_INT(3)}}there": r"[0-9][0-9][0-9]there", | |
255 "hi{{RAND_INT(3)}}": r"hi[0-9][0-9][0-9]", | |
256 "hi{{RAND_INT(3)}}there": r"hi[0-9][0-9][0-9]there", | |
257 "{{RAND_DBL(0.0)}}": r"\.", | |
258 "hi{{RAND_DBL(0.0)}}": r"hi\.", | |
259 "{{RAND_DBL(0.0)}}there": r"\.there", | |
260 "hi{{RAND_DBL(0.0)}}there": r"hi\.there", | |
261 "{{RAND_DBL(3.3)}}": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
262 "hi{{RAND_DBL(3.3)}}": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
263 "{{RAND_DBL(3.3)}}there": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
264 "hi{{RAND_DBL(3.3)}}there": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
265 "{{RAND_BOOL}}": r"(true|false)", | |
266 "{{RAND_BOOL}}there": r"(true|false)there", | |
267 "hi{{RAND_BOOL}}": r"hi(true|false)", | |
268 "hi{{RAND_BOOL}}there": r"hi(true|false)there", | |
269 "{{RAND_STR(1)}}{{RAND_STR(1)}}": r"[a-z0-9][a-z0-9]", | |
270 "{{RAND_STR(1)}}{{RAND_INT(1)}}": r"[a-z0-9][0-9]", | |
271 "{{RAND_STR(1)}}{{RAND_DBL(1.1)}}": r"[a-z0-9][0-9]\.[0-9]", | |
272 "{{RAND_STR(1)}}{{RAND_BOOL}}": r"[a-z0-9](true|false)", | |
273 "{{RAND_INT(1)}}{{RAND_STR(1)}}": r"[0-9][a-z0-9]", | |
274 "{{RAND_INT(1)}}{{RAND_INT(1)}}": r"[0-9][0-9]", | |
275 "{{RAND_INT(1)}}{{RAND_DBL(1.1)}}": r"[0-9][0-9]\.[0-9]", | |
276 "{{RAND_INT(1)}}{{RAND_BOOL}}": r"[0-9](true|false)", | |
277 "{{RAND_DBL(1.1)}}{{RAND_STR(1)}}": r"[0-9]\.[0-9][a-z0-9]", | |
278 "{{RAND_DBL(1.1)}}{{RAND_INT(1)}}": r"[0-9]\.[0-9][0-9]", | |
279 "{{RAND_DBL(1.1)}}{{RAND_DBL(1.1)}}": r"[0-9]\.[0-9][0-9]\.[0-9]", | |
280 "{{RAND_DBL(1.1)}}{{RAND_BOOL}}": r"[0-9]\.[0-9](true|false)", | |
281 "{{RAND_BOOL}}{{RAND_STR(1)}}": r"(true|false)[a-z0-9]", | |
282 "{{RAND_BOOL}}{{RAND_INT(1)}}": r"(true|false)[0-9]", | |
283 "{{RAND_BOOL}}{{RAND_DBL(1.1)}}": r"(true|false)[0-9]\.[0-9]", | |
284 "{{RAND_BOOL}}{{RAND_BOOL}}": r"(true|false)(true|false)", | |
285 } | |
286 for t in tests: | |
287 expected = re.compile('^' + tests[t] + '$') | |
288 gotK, gotV = ParseRand(t, t) | |
289 if not expected.match(gotK): | |
290 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
291 if not expected.match(gotV): | |
292 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
293 | |
294 def testGenerator(self): | |
295 test_cfg = { | |
296 "chrome-proxy": { | |
297 "directive_keys": [ | |
298 "foo", | |
299 "page_policies", | |
300 ], | |
301 "directive_values": [ | |
302 "bar", | |
303 "empty-image", | |
304 ], | |
305 "max_directives": 10, | |
306 "max_directive_values": 10, | |
307 "directive_value_joiner": "|", | |
308 }, | |
309 } | |
310 expected_headers = ['', 'foo', 'foo=bar', 'foo=empty-image', | |
311 'foo=bar|empty-image', 'page_policies', 'page_policies=bar', | |
312 'page_policies=empty-image', 'page_policies=bar|empty-image', | |
313 'foo page_policies', 'foo=bar page_policies=bar', | |
314 'foo=empty-image page_policies=empty-image', | |
315 'foo=bar|empty-image page_policies=bar|empty-image', | |
316 ] | |
317 actual_headers = [] | |
318 for h in GenerateFuzzedHeaders(cfg=test_cfg): | |
319 actual_headers.append(h[1]) | |
320 expected_headers.sort() | |
321 actual_headers.sort() | |
322 self.assertEqual(expected_headers, actual_headers) | |
323 | |
324 class ProtocolFuzzer(IntegrationTest): | |
325 | |
326 def GenerateTestURLs(self): | |
327 """This function yields test URLs which will cause the test server to | |
328 respond with the given given headers and body. | |
329 | |
330 Yields: | |
331 URLs suitable for testing fuzzed response headers | |
332 """ | |
333 for fz_key, fz_val in GenerateFuzzedHeaders(): | |
334 headers = {} | |
335 headers.update(STATIC_RESPONSE_HEADERS) | |
336 headers.update({fz_key: [fz_val]}) | |
337 json_headers = json.dumps(headers) | |
338 b64_headers = base64.b64encode(json_headers) | |
339 url = "http://%s/default?respBody=%s&respHeader=%s" % (TEST_SERVER, | |
340 base64.b64encode(STATIC_RESPONSE_BODY), b64_headers) | |
341 yield (json_headers, url) | |
342 | |
343 @Slow | |
344 def testFuzzing(self): | |
345 for headers, url in self.GenerateTestURLs(): | |
346 with TestDriver() as t: | |
347 t.AddChromeArg('--enable-spdy-proxy-auth') | |
348 t.AddChromeArg('--data-reduction-proxy-http-proxies=' | |
349 'https://chromeproxy-test.appspot.com') | |
350 try: | |
351 t.LoadURL(url) | |
352 # The main test is to make sure Chrome doesn't crash after loading a | |
353 # page with fuzzed headers, which would be raised as a ChromeDriver | |
354 # exception. Otherwise, we'll do a simple check and make sure the page | |
355 # body is correct and Chrome isn't displaying some kind of error page. | |
356 body = t.ExecuteJavascriptStatement('document.body.innerHTML') | |
357 self.assertEqual(body, STATIC_RESPONSE_BODY) | |
358 except Exception as e: | |
359 print 'Response headers: ' + headers | |
360 print 'URL: ' + url | |
361 raise e | |
362 | |
363 if __name__ == '__main__': | |
364 IntegrationTest.RunAllTests() | |
OLD | NEW |