gruyere.py 1 #!/usr/bin/env python2.7 2 3 """Gruyere - a web application with holes. 4 5 Copyright 2017 Google Inc. All rights reserved. 6 7 This code is licensed under the 8 https://creativecommons.org/licenses/by-nd/3.0/us/ 9 Creative Commons Attribution-No Derivative Works 3.0 United States license. 10 11 DO NOT COPY THIS CODE! 12 13 This application is a small self-contained web application with numerous 14 security holes. It is provided for use with the Web Application Exploits and 15 Defenses codelab. You may modify the code for your own use while doing the 16 codelab but you may not distribute the modified code. Brief excerpts of this 17 code may be used for educational or instructional purposes provided this 18 notice is kept intact. By using Gruyere you agree to the Terms of Service 19 https://www.google.com/intl/en/policies/terms/ 20 """ 21 22 __author__ = 'Bruce Leban' 23 24 # system modules 25 from BaseHTTPServer import BaseHTTPRequestHandler 26 from BaseHTTPServer import HTTPServer 27 import cgi 28 import cPickle 29 import os 30 import random 31 import sys 32 import threading 33 import urllib 34 from urlparse import urlparse 35 36 try: 37 sys.dont_write_bytecode = True 38 except AttributeError: 39 pass 40 41 # our modules 42 import data 43 import gtl 44 45 46 DB_FILE = '/stored-data.txt' 47 SECRET_FILE = '/secret.txt' 48 49 INSTALL_PATH = '.' 50 RESOURCE_PATH = 'resources' 51 52 SPECIAL_COOKIE = '_cookie' 53 SPECIAL_PROFILE = '_profile' 54 SPECIAL_DB = '_db' 55 SPECIAL_PARAMS = '_params' 56 SPECIAL_UNIQUE_ID = '_unique_id' 57 58 COOKIE_UID = 'uid' 59 COOKIE_ADMIN = 'is_admin' 60 COOKIE_AUTHOR = 'is_author' 61 62 63 # Set to True to cause the server to exit after processing the current url. 64 quit_server = False 65 66 # A global copy of the database so that _GetDatabase can access it. 67 stored_data = None 68 69 # The HTTPServer object. 70 http_server = None 71 72 # A secret value used to generate hashes to protect cookies from tampering. 73 cookie_secret = '' 74 75 # File extensions of resource files that we recognize. 76 RESOURCE_CONTENT_TYPES = { 77 '.css': 'text/css', 78 '.gif': 'image/gif', 79 '.htm': 'text/html', 80 '.html': 'text/html', 81 '.js': 'application/javascript', 82 '.jpeg': 'image/jpeg', 83 '.jpg': 'image/jpeg', 84 '.png': 'image/png', 85 '.ico': 'image/x-icon', 86 '.text': 'text/plain', 87 '.txt': 'text/plain', 88 } 89 90 91 def main(): 92 _SetWorkingDirectory() 93 94 global quit_server 95 quit_server = False 96 97 # Normally, Gruyere only accepts connections to/from localhost. If you 98 # would like to allow access from other ip addresses, you can change to 99 # operate in a less secure mode. Set insecure_mode to True to serve on the 100 # hostname instead of localhost and add the addresses of the other machines 101 # to allowed_ips below. 102 103 insecure_mode = False 104 105 # WARNING! DO NOT CHANGE THE FOLLOWING SECTION OF CODE! 106 107 # This application is very exploitable. It takes several precautions to 108 # limit the risk from a real attacker: 109 # (1) Serve requests on localhost so that it will not be accessible 110 # from other machines. 111 # (2) If a request is received from any IP other than localhost, quit. 112 # (This protection is implemented in do_GET/do_POST.) 113 # (3) Inject a random identifier as the first part of the path and 114 # quit if a request is received without this identifier (except for an 115 # empty path which redirects and /favicon.ico). 116 # (4) Automatically exit after 2 hours (7200 seconds) to mitigate against 117 # accidentally leaving the server running. 118 119 quit_timer = threading.Timer(7200, lambda: _Exit('Timeout')) # DO NOT CHANGE 120 quit_timer.start() # DO NOT CHANGE 121 122 if insecure_mode: # DO NOT CHANGE 123 server_name = os.popen('hostname').read().replace('\n', '') # DO NOT CHANGE 124 else: # DO NOT CHANGE 125 server_name = '127.0.0.1' # DO NOT CHANGE 126 server_port = 8008 # DO NOT CHANGE 127 128 # The unique id is created from a CSPRNG. 129 try: # DO NOT CHANGE 130 r = random.SystemRandom() # DO NOT CHANGE 131 except NotImplementedError: # DO NOT CHANGE 132 _Exit('Could not obtain a CSPRNG source') # DO NOT CHANGE 133 134 global server_unique_id # DO NOT CHANGE 135 server_unique_id = str(r.randint(2**128, 2**(128+1))) # DO NOT CHANGE 136 137 # END WARNING! 138 139 global http_server 140 http_server = HTTPServer((server_name, server_port), 141 GruyereRequestHandler) 142 143 print >>sys.stderr, ''' 144 Gruyere started... 145 http://%s:%d/ 146 http://%s:%d/%s/''' % ( 147 server_name, server_port, server_name, server_port, 148 server_unique_id) 149 150 global stored_data 151 stored_data = _LoadDatabase() 152 153 while not quit_server: 154 try: 155 http_server.handle_request() 156 _SaveDatabase(stored_data) 157 except KeyboardInterrupt: 158 print >>sys.stderr, '\nReceived KeyboardInterrupt' 159 quit_server = True 160 161 print >>sys.stderr, '\nClosing' 162 http_server.socket.close() 163 _Exit('quit_server') 164 165 166 def _Exit(reason): 167 # use os._exit instead of sys.exit because this can't be trapped 168 print >>sys.stderr, '\nExit: ' + reason 169 os._exit(0) 170 171 172 def _SetWorkingDirectory(): 173 """Set the working directory to the directory containing this file.""" 174 if sys.path[0]: 175 os.chdir(sys.path[0]) 176 177 178 def _LoadDatabase(): 179 """Load the database from stored-data.txt. 180 181 Returns: 182 The loaded database. 183 """ 184 185 try: 186 f = _Open(INSTALL_PATH, DB_FILE) 187 stored_data = cPickle.load(f) 188 f.close() 189 except (IOError, ValueError): 190 _Log('Couldn\'t load data; expected the first time Gruyere is run') 191 stored_data = None 192 193 f = _Open(INSTALL_PATH, SECRET_FILE) 194 global cookie_secret 195 cookie_secret = f.readline() 196 f.close() 197 198 return stored_data 199 200 201 def _SaveDatabase(save_database): 202 """Save the database to stored-data.txt. 203 204 Args: 205 save_database: the database to save. 206 """ 207 208 try: 209 f = _Open(INSTALL_PATH, DB_FILE, 'w') 210 cPickle.dump(save_database, f) 211 f.close() 212 except IOError: 213 _Log('Couldn\'t save data') 214 215 216 def _Open(location, filename, mode='rb'): 217 """Open a file from a specific location. 218 219 Args: 220 location: The directory containing the file. 221 filename: The name of the file. 222 mode: File mode for open(). 223 224 Returns: 225 A file object. 226 """ 227 return open(location + filename, mode) 228 229 230 class GruyereRequestHandler(BaseHTTPRequestHandler): 231 """Handle a http request.""" 232 233 # An empty cookie 234 NULL_COOKIE = {COOKIE_UID: None, COOKIE_ADMIN: False, COOKIE_AUTHOR: False} 235 236 # Urls that can only be accessed by administrators. 237 _PROTECTED_URLS = [ 238 '/quit', 239 '/reset' 240 ] 241 242 def _GetDatabase(self): 243 """Gets the database.""" 244 global stored_data 245 if not stored_data: 246 stored_data = data.DefaultData() 247 return stored_data 248 249 def _ResetDatabase(self): 250 """Reset the database.""" 251 # global stored_data 252 stored_data = data.DefaultData() 253 254 def _DoLogin(self, cookie, specials, params): 255 """Handles the /login url: validates the user and creates a cookie. 256 257 Args: 258 cookie: The cookie for this request. 259 specials: Other special values for this request. 260 params: Cgi parameters. 261 """ 262 database = self._GetDatabase() 263 message = '' 264 if 'uid' in params and 'pw' in params: 265 uid = self._GetParameter(params, 'uid') 266 if uid in database: 267 if database[uid]['pw'] == self._GetParameter(params, 'pw'): 268 (cookie, new_cookie_text) = ( 269 self._CreateCookie('GRUYERE', uid)) 270 self._DoHome(cookie, specials, params, new_cookie_text) 271 return 272 message = 'Invalid user name or password.' 273 # not logged in 274 specials['_message'] = message 275 self._SendTemplateResponse('/login.gtl', specials, params) 276 277 def _DoLogout(self, cookie, specials, params): 278 """Handles the /logout url: clears the cookie. 279 280 Args: 281 cookie: The cookie for this request. 282 specials: Other special values for this request. 283 params: Cgi parameters. 284 """ 285 (cookie, new_cookie_text) = ( 286 self._CreateCookie('GRUYERE', None)) 287 self._DoHome(cookie, specials, params, new_cookie_text) 288 289 def _Do(self, cookie, specials, params): 290 """Handles the home page (http://localhost/). 291 292 Args: 293 cookie: The cookie for this request. 294 specials: Other special values for this request. 295 params: Cgi parameters. 296 """ 297 self._DoHome(cookie, specials, params) 298 299 def _DoHome(self, cookie, specials, params, new_cookie_text=None): 300 """Renders the home page. 301 302 Args: 303 cookie: The cookie for this request. 304 specials: Other special values for this request. 305 params: Cgi parameters. 306 new_cookie_text: New cookie. 307 """ 308 database = self._GetDatabase() 309 specials[SPECIAL_COOKIE] = cookie 310 if cookie and cookie.get(COOKIE_UID): 311 specials[SPECIAL_PROFILE] = database.get(cookie[COOKIE_UID]) 312 else: 313 specials.pop(SPECIAL_PROFILE, None) 314 self._SendTemplateResponse( 315 '/home.gtl', specials, params, new_cookie_text) 316 317 def _DoBadUrl(self, path, cookie, specials, params): 318 """Handles invalid urls: displays an appropriate error message. 319 320 Args: 321 path: The invalid url. 322 cookie: The cookie for this request. 323 specials: Other special values for this request. 324 params: Cgi parameters. 325 """ 326 self._SendError('Invalid request: %s' % (path,), cookie, specials, params) 327 328 def _DoQuitserver(self, cookie, specials, params): 329 """Handles the /quitserver url for administrators to quit the server. 330 331 Args: 332 cookie: The cookie for this request. (unused) 333 specials: Other special values for this request. (unused) 334 params: Cgi parameters. (unused) 335 """ 336 global quit_server 337 quit_server = True 338 self._SendTextResponse('Server quit.', None) 339 340 def _AddParameter(self, name, params, data_dict, default=None): 341 """Transfers a value (with a default) from the parameters to the data.""" 342 if params.get(name): 343 data_dict[name] = params[name][0] 344 elif default is not None: 345 data_dict[name] = default 346 347 def _GetParameter(self, params, name, default=None): 348 """Gets a parameter value with a default.""" 349 if params.get(name): 350 return params[name][0] 351 return default 352 353 def _GetSnippets(self, cookie, specials, create=False): 354 """Returns all of the user's snippets.""" 355 database = self._GetDatabase() 356 try: 357 profile = database[cookie[COOKIE_UID]] 358 if create and 'snippets' not in profile: 359 profile['snippets'] = [] 360 snippets = profile['snippets'] 361 except (KeyError, TypeError): 362 _Log('Error getting snippets') 363 return None 364 return snippets 365 366 def _DoNewsnippet2(self, cookie, specials, params): 367 """Handles the /newsnippet2 url: actually add the snippet. 368 369 Args: 370 cookie: The cookie for this request. 371 specials: Other special values for this request. 372 params: Cgi parameters. 373 """ 374 snippet = self._GetParameter(params, 'snippet') 375 if not snippet: 376 self._SendError('No snippet!', cookie, specials, params) 377 else: 378 snippets = self._GetSnippets(cookie, specials, True) 379 if snippets is not None: 380 snippets.insert(0, snippet) 381 self._SendRedirect('/snippets.gtl', specials[SPECIAL_UNIQUE_ID]) 382 383 def _DoDeletesnippet(self, cookie, specials, params): 384 """Handles the /deletesnippet url: delete the indexed snippet. 385 386 Args: 387 cookie: The cookie for this request. 388 specials: Other special values for this request. 389 params: Cgi parameters. 390 """ 391 index = self._GetParameter(params, 'index') 392 snippets = self._GetSnippets(cookie, specials) 393 try: 394 del snippets[int(index)] 395 except (IndexError, TypeError, ValueError): 396 self._SendError( 397 'Invalid index (%s)' % (index,), 398 cookie, specials, params) 399 return 400 self._SendRedirect('/snippets.gtl', specials[SPECIAL_UNIQUE_ID]) 401 402 def _DoSaveprofile(self, cookie, specials, params): 403 """Saves the user's profile. 404 405 Args: 406 cookie: The cookie for this request. 407 specials: Other special values for this request. 408 params: Cgi parameters. 409 410 If the 'action' cgi parameter is 'new', then this is creating a new user 411 and it's an error if the user already exists. If action is 'update', then 412 this is editing an existing user's profile and it's an error if the user 413 does not exist. 414 """ 415 416 # build new profile 417 profile_data = {} 418 uid = self._GetParameter(params, 'uid', cookie[COOKIE_UID]) 419 newpw = self._GetParameter(params, 'pw') 420 self._AddParameter('name', params, profile_data, uid) 421 self._AddParameter('pw', params, profile_data) 422 self._AddParameter('is_author', params, profile_data) 423 self._AddParameter('is_admin', params, profile_data) 424 self._AddParameter('private_snippet', params, profile_data) 425 self._AddParameter('icon', params, profile_data) 426 self._AddParameter('web_site', params, profile_data) 427 self._AddParameter('color', params, profile_data) 428 429 # Each case below has to set either error or redirect 430 database = self._GetDatabase() 431 message = None 432 new_cookie_text = None 433 action = self._GetParameter(params, 'action') 434 if action == 'new': 435 if uid in database: 436 message = 'User already exists.' 437 else: 438 profile_data['pw'] = newpw 439 database[uid] = profile_data 440 (cookie, new_cookie_text) = self._CreateCookie('GRUYERE', uid) 441 message = 'Account created.' # error message can also indicates success 442 elif action == 'update': 443 if uid not in database: 444 message = 'User does not exist.' 445 elif (newpw and database[uid]['pw'] != self._GetParameter(params, 'oldpw') 446 and not cookie.get(COOKIE_ADMIN)): 447 # must be admin or supply old pw to change password 448 message = 'Incorrect password.' 449 else: 450 if newpw: 451 profile_data['pw'] = newpw 452 database[uid].update(profile_data) 453 redirect = '/' 454 else: 455 message = 'Invalid request' 456 _Log('SetProfile(%s, %s): %s' %(str(uid), str(action), str(message))) 457 if message: 458 self._SendError(message, cookie, specials, params, new_cookie_text) 459 else: 460 self._SendRedirect(redirect, specials[SPECIAL_UNIQUE_ID]) 461 462 def _SendHtmlResponse(self, html, new_cookie_text=None): 463 """Sends the provided html response with appropriate headers. 464 465 Args: 466 html: The response. 467 new_cookie_text: New cookie to set. 468 """ 469 self.send_response(200) 470 self.send_header('Content-type', 'text/html') 471 self.send_header('Pragma', 'no-cache') 472 if new_cookie_text: 473 self.send_header('Set-Cookie', new_cookie_text) 474 self.send_header('X-XSS-Protection', '0') 475 self.end_headers() 476 self.wfile.write(html) 477 478 def _SendTextResponse(self, text, new_cookie_text=None): 479 """Sends a verbatim text response.""" 480 481 self._SendHtmlResponse('<pre>' + cgi.escape(text) + '</pre>', 482 new_cookie_text) 483 484 def _SendTemplateResponse(self, filename, specials, params, 485 new_cookie_text=None): 486 """Sends a response using a gtl template. 487 488 Args: 489 filename: The template file. 490 specials: Other special values for this request. 491 params: Cgi parameters. 492 new_cookie_text: New cookie to set. 493 """ 494 f = None 495 try: 496 f = _Open(RESOURCE_PATH, filename) 497 template = f.read() 498 finally: 499 if f: f.close() 500 self._SendHtmlResponse( 501 gtl.ExpandTemplate(template, specials, params), 502 new_cookie_text) 503 504 def _SendFileResponse(self, filename, cookie, specials, params): 505 """Sends the contents of a file. 506 507 Args: 508 filename: The file to send. 509 cookie: The cookie for this request. 510 specials: Other special values for this request. 511 params: Cgi parameters. 512 """ 513 content_type = None 514 if filename.endswith('.gtl'): 515 self._SendTemplateResponse(filename, specials, params) 516 return 517 518 name_only = filename[filename.rfind('/'):] 519 extension = name_only[name_only.rfind('.'):] 520 if '.' not in extension: 521 content_type = 'text/plain' 522 elif extension in RESOURCE_CONTENT_TYPES: 523 content_type = RESOURCE_CONTENT_TYPES[extension] 524 else: 525 self._SendError( 526 'Unrecognized file type (%s).' % (filename,), 527 cookie, specials, params) 528 return 529 f = None 530 try: 531 f = _Open(RESOURCE_PATH, filename, 'rb') 532 self.send_response(200) 533 self.send_header('Content-type', content_type) 534 # Always cache static resources 535 self.send_header('Cache-control', 'public, max-age=7200') 536 self.send_header('X-XSS-Protection', '0') 537 self.end_headers() 538 self.wfile.write(f.read()) 539 finally: 540 if f: f.close() 541 542 def _SendError(self, message, cookie, specials, params, new_cookie_text=None): 543 """Sends an error message (using the error.gtl template). 544 545 Args: 546 message: The error to display. 547 cookie: The cookie for this request. (unused) 548 specials: Other special values for this request. 549 params: Cgi parameters. 550 new_cookie_text: New cookie to set. 551 """ 552 specials['_message'] = message 553 self._SendTemplateResponse( 554 '/error.gtl', specials, params, new_cookie_text) 555 556 def _CreateCookie(self, cookie_name, uid): 557 """Creates a cookie for this user. 558 559 Args: 560 cookie_name: Cookie to create. 561 uid: The user. 562 563 Returns: 564 (cookie, new_cookie_text). 565 566 The cookie contains all the information we need to know about 567 the user for normal operations, including whether or not the user 568 should have access to the authoring pages or the admin pages. 569 The cookie is signed with a hash function. 570 """ 571 if uid is None: 572 return (self.NULL_COOKIE, cookie_name + '=; path=/') 573 database = self._GetDatabase() 574 profile = database[uid] 575 if profile.get('is_author', False): 576 is_author = 'author' 577 else: 578 is_author = '' 579 if profile.get('is_admin', False): 580 is_admin = 'admin' 581 else: 582 is_admin = '' 583 584 c = {COOKIE_UID: uid, COOKIE_ADMIN: is_admin, COOKIE_AUTHOR: is_author} 585 c_data = '%s|%s|%s' % (uid, is_admin, is_author) 586 587 # global cookie_secret; only use positive hash values 588 h_data = str(hash(cookie_secret + c_data) & 0x7FFFFFF) 589 c_text = '%s=%s|%s; path=/' % (cookie_name, h_data, c_data) 590 return (c, c_text) 591 592 def _GetCookie(self, cookie_name): 593 """Reads, verifies and parses the cookie. 594 595 Args: 596 cookie_name: The cookie to get. 597 598 Returns: 599 a dict containing user, is_admin, and is_author if the cookie 600 is present and valid. Otherwise, None. 601 """ 602 cookies = self.headers.get('Cookie') 603 if isinstance(cookies, str): 604 for c in cookies.split(';'): 605 matched_cookie = self._MatchCookie(cookie_name, c) 606 if matched_cookie: 607 return self._ParseCookie(matched_cookie) 608 return self.NULL_COOKIE 609 610 def _MatchCookie(self, cookie_name, cookie): 611 """Matches the cookie. 612 613 Args: 614 cookie_name: The name of the cookie. 615 cookie: The full cookie (name=value). 616 617 Returns: 618 The cookie if it matches or None if it doesn't match. 619 """ 620 try: 621 (cn, cd) = cookie.strip().split('=', 1) 622 if cn != cookie_name: 623 return None 624 except (IndexError, ValueError): 625 return None 626 return cd 627 628 def _ParseCookie(self, cookie): 629 """Parses the cookie and returns NULL_COOKIE if it's invalid. 630 631 Args: 632 cookie: The text of the cookie. 633 634 Returns: 635 A map containing the values in the cookie. 636 """ 637 try: 638 (hashed, cookie_data) = cookie.split('|', 1) 639 # global cookie_secret 640 if hashed != str(hash(cookie_secret + cookie_data) & 0x7FFFFFF): 641 return self.NULL_COOKIE 642 values = cookie_data.split('|') 643 return { 644 COOKIE_UID: values[0], 645 COOKIE_ADMIN: values[1] == 'admin', 646 COOKIE_AUTHOR: values[2] == 'author', 647 } 648 except (IndexError, ValueError): 649 return self.NULL_COOKIE 650 651 def _DoReset(self, cookie, specials, params): # debug only; resets this db 652 """Handles the /reset url for administrators to reset the database. 653 654 Args: 655 cookie: The cookie for this request. (unused) 656 specials: Other special values for this request. (unused) 657 params: Cgi parameters. (unused) 658 """ 659 self._ResetDatabase() 660 self._SendTextResponse('Server reset to default values...', None) 661 662 def _DoUpload2(self, cookie, specials, params): 663 """Handles the /upload2 url: finish the upload and save the file. 664 665 Args: 666 cookie: The cookie for this request. 667 specials: Other special values for this request. 668 params: Cgi parameters. (unused) 669 """ 670 (filename, file_data) = self._ExtractFileFromRequest() 671 directory = self._MakeUserDirectory(cookie[COOKIE_UID]) 672 673 message = None 674 url = None 675 try: 676 f = _Open(directory, filename, 'wb') 677 f.write(file_data) 678 f.close() 679 (host, port) = http_server.server_address 680 url = 'http://%s:%d/%s/%s/%s' % ( 681 host, port, specials[SPECIAL_UNIQUE_ID], cookie[COOKIE_UID], filename) 682 except IOError, ex: 683 message = 'Couldn\'t write file %s: %s' % (filename, ex.message) 684 _Log(message) 685 686 specials['_message'] = message 687 self._SendTemplateResponse( 688 '/upload2.gtl', specials, 689 {'url': url}) 690 691 def _ExtractFileFromRequest(self): 692 """Extracts the file from an upload request. 693 694 Returns: 695 (filename, file_data) 696 """ 697 form = cgi.FieldStorage( 698 fp=self.rfile, 699 headers=self.headers, 700 environ={'REQUEST_METHOD': 'POST', 701 'CONTENT_TYPE': self.headers.getheader('content-type')}) 702 703 upload_file = form['upload_file'] 704 file_data = upload_file.file.read() 705 return (upload_file.filename, file_data) 706 707 def _MakeUserDirectory(self, uid): 708 """Creates a separate directory for each user to avoid upload conflicts. 709 710 Args: 711 uid: The user to create a directory for. 712 713 Returns: 714 The new directory path (/uid/). 715 """ 716 717 directory = RESOURCE_PATH + os.sep + str(uid) + os.sep 718 try: 719 print 'mkdir: ', directory 720 os.mkdir(directory) 721 # throws an exception if directory already exists, 722 # however exception type varies by platform 723 except Exception: 724 pass # just ignore it if it already exists 725 return directory 726 727 def _SendRedirect(self, url, unique_id): 728 """Sends a 302 redirect. 729 730 Automatically adds the unique_id. 731 732 Args: 733 url: The location to redirect to which must start with '/'. 734 unique_id: The unique id to include in the url. 735 """ 736 if not url: 737 url = '/' 738 url = '/' + unique_id + url 739 self.send_response(302) 740 self.send_header('Location', url) 741 self.send_header('Pragma', 'no-cache') 742 self.send_header('Content-type', 'text/html') 743 self.send_header('X-XSS-Protection', '0') 744 self.end_headers() 745 self.wfile.write( 746 '''<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML//EN'> 747 <html><body> 748 <title>302 Redirect</title> 749 Redirected <a href="%s">here</a> 750 </body></html>''' 751 % (url,)) 752 753 def _GetHandlerFunction(self, path): 754 try: 755 return getattr(GruyereRequestHandler, '_Do' + path[1:].capitalize()) 756 except AttributeError: 757 return None 758 759 def do_POST(self): # part of BaseHTTPRequestHandler interface 760 self.DoGetOrPost() 761 762 def do_GET(self): # part of BaseHTTPRequestHandler interface 763 self.DoGetOrPost() 764 765 def DoGetOrPost(self): 766 """Validate an http get or post request and call HandleRequest.""" 767 768 url = urlparse(self.path) 769 path = url[2] 770 query = url[4] 771 772 # Normally, Gruyere only accepts connections to/from localhost. If you 773 # would like to allow access from other ip addresses, add the addresses 774 # of the other machines to allowed_ips and change insecure_mode to True 775 # above. This makes the application more vulnerable to a real attack so 776 # you should only add ips of machines you completely control and make 777 # sure that you are not using them to access any other web pages while 778 # you are using Gruyere. 779 780 allowed_ips = ['127.0.0.1'] 781 782 # WARNING! DO NOT CHANGE THE FOLLOWING SECTION OF CODE! 783 784 # This application is very exploitable. See main for details. What we're 785 # doing here is (2) and (3) on the previous list: 786 # (2) If a request is received from any IP other than localhost, quit. 787 # An external attacker could still mount an attack on this IP by putting 788 # an attack on an external web page, e.g., a web page that redirects to 789 # a vulnerable url on 127.0.0.1 (which is why we use a random number). 790 # (3) Inject a random identifier as the first part of the path and 791 # quit if a request is received without this identifier (except for an 792 # empty path which redirects and /favicon.ico). 793 794 request_ip = self.client_address[0] # DO NOT CHANGE 795 if request_ip not in allowed_ips: # DO NOT CHANGE 796 print >>sys.stderr, ( # DO NOT CHANGE 797 'DANGER! Request from bad ip: ' + request_ip) # DO NOT CHANGE 798 _Exit('bad_ip') # DO NOT CHANGE 799 800 if (server_unique_id not in path # DO NOT CHANGE 801 and path != '/favicon.ico'): # DO NOT CHANGE 802 if path == '' or path == '/': # DO NOT CHANGE 803 self._SendRedirect('/', server_unique_id) # DO NOT CHANGE 804 return # DO NOT CHANGE 805 else: # DO NOT CHANGE 806 print >>sys.stderr, ( # DO NOT CHANGE 807 'DANGER! Request without unique id: ' + path) # DO NOT CHANGE 808 _Exit('bad_id') # DO NOT CHANGE 809 810 path = path.replace('/' + server_unique_id, '', 1) # DO NOT CHANGE 811 812 # END WARNING! 813 814 self.HandleRequest(path, query, server_unique_id) 815 816 def HandleRequest(self, path, query, unique_id): 817 """Handles an http request. 818 819 Args: 820 path: The path part of the url, with leading slash. 821 query: The query part of the url, without leading question mark. 822 unique_id: The unique id from the url. 823 """ 824 825 path = urllib.unquote(path) 826 827 if not path: 828 self._SendRedirect('/', server_unique_id) 829 return 830 params = cgi.parse_qs(query) # url.query 831 specials = {} 832 cookie = self._GetCookie('GRUYERE') 833 database = self._GetDatabase() 834 specials[SPECIAL_COOKIE] = cookie 835 specials[SPECIAL_DB] = database 836 specials[SPECIAL_PROFILE] = database.get(cookie.get(COOKIE_UID)) 837 specials[SPECIAL_PARAMS] = params 838 specials[SPECIAL_UNIQUE_ID] = unique_id 839 840 if path in self._PROTECTED_URLS and not cookie[COOKIE_ADMIN]: 841 self._SendError('Invalid request', cookie, specials, params) 842 return 843 844 try: 845 handler = self._GetHandlerFunction(path) 846 if callable(handler): 847 (handler)(self, cookie, specials, params) 848 else: 849 try: 850 self._SendFileResponse(path, cookie, specials, params) 851 except IOError: 852 self._DoBadUrl(path, cookie, specials, params) 853 except KeyboardInterrupt: 854 _Exit('KeyboardInterrupt') 855 856 857 def _Log(message): 858 print >>sys.stderr, message 859 860 861 if __name__ == '__main__': 862 main()