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 | |
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 "{{RAND_STR(10)}}", | |
102 "{{RAND_STR(10)}}", | |
Tom Bergan
2017/06/08 15:00:18
Do we actually need two of these?
Robert Ogden
2017/06/08 17:17:58
I was thinking yes so that we get three directives
| |
103 "page-policies", | |
104 ], | |
105 "directive_values": [ | |
106 "empty-image", | |
107 "{{RAND_INT(1)}}", | |
108 "{{RAND_INT(4)}}", | |
109 "{{RAND_STR(10)}}", | |
110 "{{RAND_STR(10)}}", | |
111 "{{RAND_STR(10)}}", | |
Tom Bergan
2017/06/08 15:00:18
Ditto here
Robert Ogden
2017/06/08 17:17:58
Yes, so that there are three random strings in at
| |
112 ], | |
113 "max_directives": 3, | |
114 "max_directive_values": 3, | |
115 "directive_value_joiner": "|", | |
116 }, | |
117 } | |
118 | |
119 TEST_SERVER = "chromeproxy-test.appspot.com" | |
120 | |
121 # These headers will be present in every test server response. If one of these | |
122 # entries is also a fuzzed header above, then the fuzzed value will take the | |
123 # place of the static one instead. | |
124 STATIC_RESPONSE_HEADERS = { | |
125 "content-type": ["text/html"], | |
126 "via": ["1.1 Chrome-Compression-Proxy"], | |
127 "cache-control": ["no-cache, no-store, must-revalidate"], | |
128 "pragma": ["no-cache"], | |
129 "expires": ["0"], | |
130 } | |
131 | |
132 # This string will be used as the response body in every test and will be | |
133 # checked for existance on the final loaded page. | |
134 STATIC_RESPONSE_BODY = 'ok' | |
135 | |
136 rand_str_re = re.compile(r'{{RAND_STR\((\d+)\)}}') | |
137 rand_int_re = re.compile(r'{{RAND_INT\((\d+)\)}}') | |
138 rand_dbl_re = re.compile(r'{{RAND_DBL\((\d+)\.(\d+)\)}}') | |
139 rand_bool_re = re.compile(r'{{RAND_BOOL}}') | |
140 | |
141 def ParseRand(key, val): | |
142 """This helper function parses the {{RAND}} expressions in the given values | |
143 and returns them with random values subsituted in place. | |
144 | |
145 Args: | |
146 key: the header key with 0 or more {{RAND}} expressions | |
147 val: the header value with 0 or more {{RAND}} expressions | |
148 Returns: | |
149 A key, value tuple with subsituted random values | |
150 """ | |
151 def GenerateRand(length, charset): | |
152 return ''.join(random.choice(charset) for _ in range(length)) | |
153 def _parse_rand(v): | |
154 result = v | |
155 had_match = True | |
156 while had_match: | |
157 had_match = False | |
158 str_match = rand_str_re.search(result) | |
159 if str_match: | |
160 had_match = True | |
161 mag = int(result[str_match.start(1):str_match.end(1)]) | |
162 rand_str = GenerateRand(mag, string.ascii_lowercase + string.digits) | |
163 result = (result[:str_match.start()] + rand_str | |
164 + result[str_match.end():]) | |
165 int_match = rand_int_re.search(result) | |
166 if int_match: | |
167 had_match = True | |
168 mag = int(result[int_match.start(1):int_match.end(1)]) | |
169 rand_int = GenerateRand(mag, string.digits) | |
170 result = (result[:int_match.start()] + rand_int | |
171 + result[int_match.end():]) | |
172 dbl_match = rand_dbl_re.search(result) | |
173 if dbl_match: | |
174 had_match = True | |
175 magN = int(result[dbl_match.start(1):dbl_match.end(1)]) | |
176 magD = int(result[dbl_match.start(2):dbl_match.end(2)]) | |
177 rand_dbl = GenerateRand(magN, string.digits) + '.' + GenerateRand(magD, | |
178 string.digits) | |
179 result = (result[:dbl_match.start()] + rand_dbl | |
180 + result[dbl_match.end():]) | |
181 bool_match = rand_bool_re.search(result) | |
182 if bool_match: | |
183 had_match = True | |
184 rand_bool = bool(random.getrandbits(1)) | |
185 result = (result[:bool_match.start()] + str(rand_bool).lower() | |
186 + result[bool_match.end():]) | |
187 return result | |
188 return (_parse_rand(key), _parse_rand(val)) | |
189 | |
190 def GenerateFuzzedHeaders(cfg=FUZZ_HEADERS): | |
191 """This function yields header key value pairs which can be used to update a | |
192 Python dict representing HTTP headers. See file level documentation for more | |
193 information. | |
194 | |
195 Args: | |
196 cfg: the configuration dict that specifies how to fuzz the proxy headers | |
197 Yields: | |
198 one header key value pair | |
199 """ | |
200 for header_key in cfg: | |
201 fuzz = cfg[header_key] | |
202 dirs = fuzz['directive_keys'] | |
203 vals = fuzz['directive_values'] | |
204 max_dirs = min(fuzz['max_directives'], len(dirs)) | |
205 max_vals = min(fuzz['max_directive_values'], len(vals)) | |
206 def GenerateFuzzedValues(): | |
207 for n in range(0, max_vals + 1): | |
208 for c in itertools.combinations(vals, n): | |
209 yield c | |
210 # Yield an empty header key,value pair before doing all the combinations. | |
211 yield (header_key, '') | |
212 for num_dirs in range(1, max_dirs + 1): | |
213 for directive_set in itertools.combinations(dirs, num_dirs): | |
214 for values in GenerateFuzzedValues(): | |
215 value_list = list(values) | |
216 if '' in value_list: | |
217 value_list.remove('') | |
218 value_str = fuzz['directive_value_joiner'].join(value_list) | |
219 header = [] | |
220 for directive in directive_set: | |
221 if len(value_str) == 0: | |
222 header.append(directive) | |
223 else: | |
224 header.append('%s=%s' % (directive, value_str)) | |
225 yield ParseRand(header_key, ','.join(header)) | |
226 | |
227 class FuzzUnitTests(IntegrationTest): | |
228 | |
229 def testParseRand(self): | |
230 tests = { | |
231 "{{RAND_STR(1)}": r"{{RAND_STR\(1\)}", | |
232 "{{RAND_INT(1)}": r"{{RAND_INT\(1\)}", | |
233 "{{RAND_DBL(1.1)}": r"{{RAND_DBL\(1\.1\)}", | |
234 "{{RAND_DBL(11)}}": r"{{RAND_DBL\(11\)}}", | |
235 "{{RAND_BOOL}": r"{{RAND_BOOL}", | |
236 "{{RAND_STR(0)}}": "", | |
237 "hi{{RAND_STR(0)}}": "hi", | |
238 "{{RAND_STR(0)}}there": "there", | |
239 "hi{{RAND_STR(0)}}there": "hithere", | |
240 "{{RAND_STR(3)}}": r"[a-z0-9][a-z0-9][a-z0-9]", | |
241 "{{RAND_STR(3)}}there": r"[a-z0-9][a-z0-9][a-z0-9]there", | |
242 "hi{{RAND_STR(3)}}": r"hi[a-z0-9][a-z0-9][a-z0-9]", | |
243 "hi{{RAND_STR(3)}}there": r"hi[a-z0-9][a-z0-9][a-z0-9]there", | |
244 "{{RAND_INT(0)}}": "", | |
245 "hi{{RAND_INT(0)}}": "hi", | |
246 "{{RAND_INT(0)}}there": "there", | |
247 "hi{{RAND_INT(0)}}there": "hithere", | |
248 "{{RAND_INT(3)}}": r"[0-9][0-9][0-9]", | |
249 "{{RAND_INT(3)}}there": r"[0-9][0-9][0-9]there", | |
250 "hi{{RAND_INT(3)}}": r"hi[0-9][0-9][0-9]", | |
251 "hi{{RAND_INT(3)}}there": r"hi[0-9][0-9][0-9]there", | |
252 "{{RAND_DBL(0.0)}}": r"\.", | |
253 "hi{{RAND_DBL(0.0)}}": r"hi\.", | |
254 "{{RAND_DBL(0.0)}}there": r"\.there", | |
255 "hi{{RAND_DBL(0.0)}}there": r"hi\.there", | |
256 "{{RAND_DBL(3.3)}}": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
257 "hi{{RAND_DBL(3.3)}}": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
258 "{{RAND_DBL(3.3)}}there": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
259 "hi{{RAND_DBL(3.3)}}there": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
260 "{{RAND_BOOL}}": r"(true|false)", | |
261 "{{RAND_BOOL}}there": r"(true|false)there", | |
262 "hi{{RAND_BOOL}}": r"hi(true|false)", | |
263 "hi{{RAND_BOOL}}there": r"hi(true|false)there", | |
264 "{{RAND_STR(1)}}{{RAND_STR(1)}}": r"[a-z0-9][a-z0-9]", | |
265 "{{RAND_STR(1)}}{{RAND_INT(1)}}": r"[a-z0-9][0-9]", | |
266 "{{RAND_STR(1)}}{{RAND_DBL(1.1)}}": r"[a-z0-9][0-9]\.[0-9]", | |
267 "{{RAND_STR(1)}}{{RAND_BOOL}}": r"[a-z0-9](true|false)", | |
268 "{{RAND_INT(1)}}{{RAND_STR(1)}}": r"[0-9][a-z0-9]", | |
269 "{{RAND_INT(1)}}{{RAND_INT(1)}}": r"[0-9][0-9]", | |
270 "{{RAND_INT(1)}}{{RAND_DBL(1.1)}}": r"[0-9][0-9]\.[0-9]", | |
271 "{{RAND_INT(1)}}{{RAND_BOOL}}": r"[0-9](true|false)", | |
272 "{{RAND_DBL(1.1)}}{{RAND_STR(1)}}": r"[0-9]\.[0-9][a-z0-9]", | |
273 "{{RAND_DBL(1.1)}}{{RAND_INT(1)}}": r"[0-9]\.[0-9][0-9]", | |
274 "{{RAND_DBL(1.1)}}{{RAND_DBL(1.1)}}": r"[0-9]\.[0-9][0-9]\.[0-9]", | |
275 "{{RAND_DBL(1.1)}}{{RAND_BOOL}}": r"[0-9]\.[0-9](true|false)", | |
276 "{{RAND_BOOL}}{{RAND_STR(1)}}": r"(true|false)[a-z0-9]", | |
277 "{{RAND_BOOL}}{{RAND_INT(1)}}": r"(true|false)[0-9]", | |
278 "{{RAND_BOOL}}{{RAND_DBL(1.1)}}": r"(true|false)[0-9]\.[0-9]", | |
279 "{{RAND_BOOL}}{{RAND_BOOL}}": r"(true|false)(true|false)", | |
280 } | |
281 for t in tests: | |
282 expected = re.compile('^' + tests[t] + '$') | |
283 gotK, gotV = ParseRand(t, t) | |
284 if not expected.match(gotK): | |
285 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
286 if not expected.match(gotV): | |
287 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
288 | |
289 def testGenerator(self): | |
290 test_cfg = { | |
291 "chrome-proxy": { | |
292 "directive_keys": [ | |
293 "foo", | |
294 "page_policies", | |
295 ], | |
296 "directive_values": [ | |
297 "bar", | |
298 "empty-image", | |
299 ], | |
300 "max_directives": 10, | |
301 "max_directive_values": 10, | |
302 "directive_value_joiner": "|", | |
303 }, | |
304 } | |
305 expected_headers = ['', 'foo', 'foo=bar', 'foo=empty-image', | |
306 'foo=bar|empty-image', 'page_policies', 'page_policies=bar', | |
307 'page_policies=empty-image', 'page_policies=bar|empty-image', | |
308 'foo,page_policies', 'foo=bar,page_policies=bar', | |
309 'foo=empty-image,page_policies=empty-image', | |
310 'foo=bar|empty-image,page_policies=bar|empty-image', | |
311 ] | |
312 actual_headers = [] | |
313 for h in GenerateFuzzedHeaders(cfg=test_cfg): | |
314 actual_headers.append(h[1]) | |
315 expected_headers.sort() | |
316 actual_headers.sort() | |
317 self.assertEqual(expected_headers, actual_headers) | |
318 | |
319 class ProtocolFuzzer(IntegrationTest): | |
320 | |
321 def GenerateTestURLs(self): | |
322 """This function yields test URLs which will cause the test server to | |
323 respond with the given given headers and body. | |
324 | |
325 Yields: | |
326 URLs suitable for testing fuzzed response headers | |
327 """ | |
328 for fz_key, fz_val in GenerateFuzzedHeaders(): | |
329 headers = {} | |
330 headers.update(STATIC_RESPONSE_HEADERS) | |
331 headers.update({fz_key: [fz_val]}) | |
332 json_headers = json.dumps(headers) | |
333 b64_headers = base64.b64encode(json_headers) | |
334 url = "http://%s/default?respBody=%s&respHeader=%s" % (TEST_SERVER, | |
335 base64.b64encode(STATIC_RESPONSE_BODY), b64_headers) | |
336 yield (json_headers, url) | |
337 | |
338 @Slow | |
339 def testFuzzing(self): | |
340 with TestDriver() as t: | |
341 t.AddChromeArg('--enable-spdy-proxy-auth') | |
342 t.AddChromeArg('--data-reduction-proxy-http-proxies=' | |
343 'https://chromeproxy-test.appspot.com') | |
344 for headers, url in self.GenerateTestURLs(): | |
345 try: | |
346 t.LoadURL(url) | |
347 # The main test is to make sure Chrome doesn't crash after loading a | |
348 # page with fuzzed headers, which would be raised as a ChromeDriver | |
349 # exception. Otherwise, we'll do a simple check and make sure the page | |
350 # body is correct and Chrome isn't displaying some kind of error page. | |
351 body = t.ExecuteJavascriptStatement('document.body.innerHTML') | |
352 self.assertEqual(body, STATIC_RESPONSE_BODY) | |
353 except Exception as e: | |
354 print 'Response headers: ' + headers | |
355 print 'URL: ' + url | |
356 raise e | |
357 | |
358 if __name__ == '__main__': | |
359 IntegrationTest.RunAllTests() | |
OLD | NEW |