diff --git a/modified_gruyere_code/gruyere.py b/modified_gruyere_code/gruyere.py
new file mode 100644
index 0000000000000000000000000000000000000000..10b4dcd1be77f4182c7e3fb9b604ae3d76d35279
--- /dev/null
+++ b/modified_gruyere_code/gruyere.py
@@ -0,0 +1,863 @@
+#!/usr/bin/env python2.7
+
+"""Gruyere - a web application with holes.
+
+Copyright 2017 Google Inc. All rights reserved.
+
+This code is licensed under the
+https://creativecommons.org/licenses/by-nd/3.0/us/
+Creative Commons Attribution-No Derivative Works 3.0 United States license.
+
+DO NOT COPY THIS CODE!
+
+This application is a small self-contained web application with numerous
+security holes. It is provided for use with the Web Application Exploits and
+Defenses codelab. You may modify the code for your own use while doing the
+codelab but you may not distribute the modified code. Brief excerpts of this
+code may be used for educational or instructional purposes provided this
+notice is kept intact. By using Gruyere you agree to the Terms of Service
+https://www.google.com/intl/en/policies/terms/
+"""
+
+__author__ = 'Bruce Leban'
+
+# system modules
+from BaseHTTPServer import BaseHTTPRequestHandler
+from BaseHTTPServer import HTTPServer
+import cgi
+import cPickle
+import os
+import random
+import sys
+import threading
+import urllib
+from urlparse import urlparse
+
+try:
+  sys.dont_write_bytecode = True
+except AttributeError:
+  pass
+
+# our modules
+import data
+import gtl
+
+
+DB_FILE = '/stored-data.txt'
+SECRET_FILE = '/secret.txt'
+
+INSTALL_PATH = '.'
+RESOURCE_PATH = 'resources'
+
+SPECIAL_COOKIE = '_cookie'
+SPECIAL_PROFILE = '_profile'
+SPECIAL_DB = '_db'
+SPECIAL_PARAMS = '_params'
+SPECIAL_UNIQUE_ID = '_unique_id'
+
+COOKIE_UID = 'uid'
+COOKIE_ADMIN = 'is_admin'
+COOKIE_AUTHOR = 'is_author'
+
+
+# Set to True to cause the server to exit after processing the current url.
+quit_server = False
+
+# A global copy of the database so that _GetDatabase can access it.
+stored_data = None
+
+# The HTTPServer object.
+http_server = None
+
+# A secret value used to generate hashes to protect cookies from tampering.
+cookie_secret = ''
+
+# File extensions of resource files that we recognize.
+RESOURCE_CONTENT_TYPES = {
+    '.css': 'text/css',
+    '.gif': 'image/gif',
+    '.htm': 'text/html',
+    '.html': 'text/html',
+    '.js': 'application/javascript',
+    '.jpeg': 'image/jpeg',
+    '.jpg': 'image/jpeg',
+    '.png': 'image/png',
+    '.ico': 'image/x-icon',
+    '.text': 'text/plain',
+    '.txt': 'text/plain',
+}
+
+
+def main():
+  _SetWorkingDirectory()
+
+  global quit_server
+  quit_server = False
+
+  # Normally, Gruyere only accepts connections to/from localhost. If you
+  # would like to allow access from other ip addresses, you can change to
+  # operate in a less secure mode. Set insecure_mode to True to serve on the
+  # hostname instead of localhost and add the addresses of the other machines
+  # to allowed_ips below.
+
+  insecure_mode = False
+
+  # WARNING! DO NOT CHANGE THE FOLLOWING SECTION OF CODE!
+
+  # This application is very exploitable. It takes several precautions to
+  # limit the risk from a real attacker:
+  #   (1) Serve requests on localhost so that it will not be accessible
+  # from other machines.
+  #   (2) If a request is received from any IP other than localhost, quit.
+  # (This protection is implemented in do_GET/do_POST.)
+  #   (3) Inject a random identifier as the first part of the path and
+  # quit if a request is received without this identifier (except for an
+  # empty path which redirects and /favicon.ico).
+  #   (4) Automatically exit after 2 hours (7200 seconds) to mitigate against
+  # accidentally leaving the server running.
+
+  quit_timer = threading.Timer(7200, lambda: _Exit('Timeout'))   # DO NOT CHANGE
+  quit_timer.start()                                             # DO NOT CHANGE
+
+  if insecure_mode:                                              # DO NOT CHANGE
+    server_name = os.popen('hostname').read().replace('\n', '')  # DO NOT CHANGE
+  else:                                                          # DO NOT CHANGE
+    server_name = '127.0.0.1'                                    # DO NOT CHANGE
+  server_port = 8008                                             # DO NOT CHANGE
+
+  # The unique id is created from a CSPRNG.
+  try:                                                           # DO NOT CHANGE
+    r = random.SystemRandom()                                    # DO NOT CHANGE
+  except NotImplementedError:                                    # DO NOT CHANGE
+    _Exit('Could not obtain a CSPRNG source')                    # DO NOT CHANGE
+
+  global server_unique_id                                        # DO NOT CHANGE
+  server_unique_id = str(r.randint(2**128, 2**(128+1)))          # DO NOT CHANGE
+
+  # END WARNING!
+
+  global http_server
+  http_server = HTTPServer((server_name, server_port),
+                           GruyereRequestHandler)
+
+  print >>sys.stderr, '''
+      Gruyere started...
+          http://%s:%d/
+          http://%s:%d/%s/''' % (
+              server_name, server_port, server_name, server_port,
+              server_unique_id)
+
+  global stored_data
+  stored_data = _LoadDatabase()
+
+  while not quit_server:
+    try:
+      http_server.handle_request()
+      _SaveDatabase(stored_data)
+    except KeyboardInterrupt:
+      print >>sys.stderr, '\nReceived KeyboardInterrupt'
+      quit_server = True
+
+  print >>sys.stderr, '\nClosing'
+  http_server.socket.close()
+  _Exit('quit_server')
+
+
+def _Exit(reason):
+  # use os._exit instead of sys.exit because this can't be trapped
+  print >>sys.stderr, '\nExit: ' + reason
+  os._exit(0)
+
+
+def _SetWorkingDirectory():
+  """Set the working directory to the directory containing this file."""
+  if sys.path[0]:
+    os.chdir(sys.path[0])
+
+
+def _LoadDatabase():
+  """Load the database from stored-data.txt.
+
+  Returns:
+    The loaded database.
+  """
+
+  try:
+    f = _Open(INSTALL_PATH, DB_FILE)
+    stored_data = cPickle.load(f)
+    f.close()
+  except (IOError, ValueError):
+    _Log('Couldn\'t load data; expected the first time Gruyere is run')
+    stored_data = None
+
+  f = _Open(INSTALL_PATH, SECRET_FILE)
+  global cookie_secret
+  cookie_secret = f.readline()
+  f.close()
+
+  return stored_data
+
+
+def _SaveDatabase(save_database):
+  """Save the database to stored-data.txt.
+
+  Args:
+    save_database: the database to save.
+  """
+
+  try:
+    f = _Open(INSTALL_PATH, DB_FILE, 'w')
+    cPickle.dump(save_database, f)
+    f.close()
+  except IOError:
+    _Log('Couldn\'t save data')
+
+
+def _Open(location, filename, mode='rb'):
+  """Open a file from a specific location.
+
+  Args:
+    location: The directory containing the file.
+    filename: The name of the file.
+    mode: File mode for open().
+
+  Returns:
+    A file object.
+  """
+  return open(location + filename, mode)
+
+
+class GruyereRequestHandler(BaseHTTPRequestHandler):
+  """Handle a http request."""
+
+  # An empty cookie
+  NULL_COOKIE = {COOKIE_UID: None, COOKIE_ADMIN: False, COOKIE_AUTHOR: False}
+
+  # Urls that can only be accessed by administrators.
+  _PROTECTED_URLS = [
+      '/quit',
+      '/reset'
+  ]
+
+  def _GetDatabase(self):
+    """Gets the database."""
+    global stored_data
+    if not stored_data:
+      stored_data = data.DefaultData()
+    return stored_data
+
+  def _ResetDatabase(self):
+    """Reset the database."""
+    # global stored_data
+    stored_data = data.DefaultData()
+
+  def _DoLogin(self, cookie, specials, params):
+    """Handles the /login url: validates the user and creates a cookie.
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+    """
+    database = self._GetDatabase()
+    message = ''
+    if 'uid' in params and 'pw' in params:
+      uid = self._GetParameter(params, 'uid')
+      if uid in database:
+        if database[uid]['pw'] == self._GetParameter(params, 'pw'):
+          (cookie, new_cookie_text) = (
+              self._CreateCookie('GRUYERE', uid))
+          self._DoHome(cookie, specials, params, new_cookie_text)
+          return
+      message = 'Invalid user name or password.'
+    # not logged in
+    specials['_message'] = message
+    self._SendTemplateResponse('/login.gtl', specials, params)
+
+  def _DoLogout(self, cookie, specials, params):
+    """Handles the /logout url: clears the cookie.
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+    """
+    (cookie, new_cookie_text) = (
+        self._CreateCookie('GRUYERE', None))
+    self._DoHome(cookie, specials, params, new_cookie_text)
+
+  def _Do(self, cookie, specials, params):
+    """Handles the home page (http://localhost/).
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+    """
+    self._DoHome(cookie, specials, params)
+
+  def _DoHome(self, cookie, specials, params, new_cookie_text=None):
+    """Renders the home page.
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+      new_cookie_text: New cookie.
+    """
+    database = self._GetDatabase()
+    specials[SPECIAL_COOKIE] = cookie
+    if cookie and cookie.get(COOKIE_UID):
+      specials[SPECIAL_PROFILE] = database.get(cookie[COOKIE_UID])
+    else:
+      specials.pop(SPECIAL_PROFILE, None)
+    self._SendTemplateResponse(
+        '/home.gtl', specials, params, new_cookie_text)
+
+  def _DoBadUrl(self, path, cookie, specials, params):
+    """Handles invalid urls: displays an appropriate error message.
+
+    Args:
+      path: The invalid url.
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+    """
+    self._SendError('Invalid request: %s' % (path,), cookie, specials, params)
+
+  def _DoQuitserver(self, cookie, specials, params):
+    """Handles the /quitserver url for administrators to quit the server.
+
+    Args:
+      cookie: The cookie for this request. (unused)
+      specials: Other special values for this request. (unused)
+      params: Cgi parameters. (unused)
+    """
+    global quit_server
+    quit_server = True
+    self._SendTextResponse('Server quit.', None)
+
+  def _AddParameter(self, name, params, data_dict, default=None):
+    """Transfers a value (with a default) from the parameters to the data."""
+    if params.get(name):
+      data_dict[name] = params[name][0]
+    elif default is not None:
+      data_dict[name] = default
+
+  def _GetParameter(self, params, name, default=None):
+    """Gets a parameter value with a default."""
+    if params.get(name):
+      return params[name][0]
+    return default
+
+  def _GetSnippets(self, cookie, specials, create=False):
+    """Returns all of the user's snippets."""
+    database = self._GetDatabase()
+    try:
+      profile = database[cookie[COOKIE_UID]]
+      if create and 'snippets' not in profile:
+        profile['snippets'] = []
+      snippets = profile['snippets']
+    except (KeyError, TypeError):
+      _Log('Error getting snippets')
+      return None
+    return snippets
+
+  def _DoNewsnippet2(self, cookie, specials, params):
+    """Handles the /newsnippet2 url: actually add the snippet.
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+    """
+    snippet = self._GetParameter(params, 'snippet')
+    if not snippet:
+      self._SendError('No snippet!', cookie, specials, params)
+    else:
+      snippets = self._GetSnippets(cookie, specials, True)
+      if snippets is not None:
+        snippets.insert(0, snippet)
+    self._SendRedirect('/snippets.gtl', specials[SPECIAL_UNIQUE_ID])
+
+  def _DoDeletesnippet(self, cookie, specials, params):
+    """Handles the /deletesnippet url: delete the indexed snippet.
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+    """
+    index = self._GetParameter(params, 'index')
+    snippets = self._GetSnippets(cookie, specials)
+    try:
+      del snippets[int(index)]
+    except (IndexError, TypeError, ValueError):
+      self._SendError(
+          'Invalid index (%s)' % (index,),
+          cookie, specials, params)
+      return
+    self._SendRedirect('/snippets.gtl', specials[SPECIAL_UNIQUE_ID])
+
+  def _DoSaveprofile(self, cookie, specials, params):
+    """Saves the user's profile.
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+
+    If the 'action' cgi parameter is 'new', then this is creating a new user
+    and it's an error if the user already exists. If action is 'update', then
+    this is editing an existing user's profile and it's an error if the user
+    does not exist.
+    """
+
+    # build new profile
+    profile_data = {}
+    uid = self._GetParameter(params, 'uid', cookie[COOKIE_UID])
+    newpw = self._GetParameter(params, 'pw')
+    self._AddParameter('name', params, profile_data, uid)
+    self._AddParameter('pw', params, profile_data)
+    self._AddParameter('is_author', params, profile_data)
+    self._AddParameter('is_admin', params, profile_data)
+    self._AddParameter('private_snippet', params, profile_data)
+    self._AddParameter('icon', params, profile_data)
+    self._AddParameter('web_site', params, profile_data)
+    self._AddParameter('color', params, profile_data)
+
+    # Each case below has to set either error or redirect
+    database = self._GetDatabase()
+    message = None
+    new_cookie_text = None
+    action = self._GetParameter(params, 'action')
+    if action == 'new':
+      if uid in database:
+        message = 'User already exists.'
+      else:
+        profile_data['pw'] = newpw
+        database[uid] = profile_data
+        (cookie, new_cookie_text) = self._CreateCookie('GRUYERE', uid)
+        message = 'Account created.'  # error message can also indicates success
+    elif action == 'update':
+      if uid not in database:
+        message = 'User does not exist.'
+      elif (newpw and database[uid]['pw'] != self._GetParameter(params, 'oldpw')
+            and not cookie.get(COOKIE_ADMIN)):
+        # must be admin or supply old pw to change password
+        message = 'Incorrect password.'
+      else:
+        if newpw:
+          profile_data['pw'] = newpw
+        database[uid].update(profile_data)
+        redirect = '/'
+    else:
+      message = 'Invalid request'
+    _Log('SetProfile(%s, %s): %s' %(str(uid), str(action), str(message)))
+    if message:
+      self._SendError(message, cookie, specials, params, new_cookie_text)
+    else:
+      self._SendRedirect(redirect, specials[SPECIAL_UNIQUE_ID])
+
+  def _SendHtmlResponse(self, html, new_cookie_text=None):
+    """Sends the provided html response with appropriate headers.
+
+    Args:
+      html: The response.
+      new_cookie_text: New cookie to set.
+    """
+    self.send_response(200)
+    self.send_header('Content-type', 'text/html')
+    self.send_header('Pragma', 'no-cache')
+    if new_cookie_text:
+      ### MODIFICATION: set cookie to HttpOnly
+      self.send_header('Set-Cookie', new_cookie_text + '; HttpOnly')
+    self.send_header('X-XSS-Protection', '0')
+    self.end_headers()
+    self.wfile.write(html)
+
+  def _SendTextResponse(self, text, new_cookie_text=None):
+    """Sends a verbatim text response."""
+
+    self._SendHtmlResponse('<pre>' + cgi.escape(text) + '</pre>',
+                           new_cookie_text)
+
+  def _SendTemplateResponse(self, filename, specials, params,
+                            new_cookie_text=None):
+    """Sends a response using a gtl template.
+
+    Args:
+      filename: The template file.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+      new_cookie_text: New cookie to set.
+    """
+    f = None
+    try:
+      f = _Open(RESOURCE_PATH, filename)
+      template = f.read()
+    finally:
+      if f: f.close()
+    self._SendHtmlResponse(
+        gtl.ExpandTemplate(template, specials, params),
+        new_cookie_text)
+
+  def _SendFileResponse(self, filename, cookie, specials, params):
+    """Sends the contents of a file.
+
+    Args:
+      filename: The file to send.
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters.
+    """
+    content_type = None
+    if filename.endswith('.gtl'):
+      self._SendTemplateResponse(filename, specials, params)
+      return
+
+    name_only = filename[filename.rfind('/'):]
+    extension = name_only[name_only.rfind('.'):]
+    if '.' not in extension:
+      content_type = 'text/plain'
+    elif extension in RESOURCE_CONTENT_TYPES:
+      content_type = RESOURCE_CONTENT_TYPES[extension]
+    else:
+      self._SendError(
+          'Unrecognized file type (%s).' % (filename,),
+          cookie, specials, params)
+      return
+    f = None
+    try:
+      f = _Open(RESOURCE_PATH, filename, 'rb')
+      self.send_response(200)
+      self.send_header('Content-type', content_type)
+      # Always cache static resources
+      self.send_header('Cache-control', 'public, max-age=7200')
+      self.send_header('X-XSS-Protection', '0')
+      self.end_headers()
+      self.wfile.write(f.read())
+    finally:
+      if f: f.close()
+
+  def _SendError(self, message, cookie, specials, params, new_cookie_text=None):
+    """Sends an error message (using the error.gtl template).
+
+    Args:
+      message: The error to display.
+      cookie: The cookie for this request. (unused)
+      specials: Other special values for this request.
+      params: Cgi parameters.
+      new_cookie_text: New cookie to set.
+    """
+    specials['_message'] = message
+    self._SendTemplateResponse(
+        '/error.gtl', specials, params, new_cookie_text)
+
+  def _CreateCookie(self, cookie_name, uid):
+    """Creates a cookie for this user.
+
+    Args:
+      cookie_name: Cookie to create.
+      uid: The user.
+
+    Returns:
+      (cookie, new_cookie_text).
+
+    The cookie contains all the information we need to know about
+    the user for normal operations, including whether or not the user
+    should have access to the authoring pages or the admin pages.
+    The cookie is signed with a hash function.
+    """
+    if uid is None:
+      return (self.NULL_COOKIE, cookie_name + '=; path=/')
+    database = self._GetDatabase()
+    profile = database[uid]
+    if profile.get('is_author', False):
+      is_author = 'author'
+    else:
+      is_author = ''
+    if profile.get('is_admin', False):
+      is_admin = 'admin'
+    else:
+      is_admin = ''
+
+    c = {COOKIE_UID: uid, COOKIE_ADMIN: is_admin, COOKIE_AUTHOR: is_author}
+    c_data = '%s|%s|%s' % (uid, is_admin, is_author)
+
+    # global cookie_secret; only use positive hash values
+    h_data = str(hash(cookie_secret + c_data) & 0x7FFFFFF)
+    c_text = '%s=%s|%s; path=/' % (cookie_name, h_data, c_data)
+    return (c, c_text)
+
+  def _GetCookie(self, cookie_name):
+    """Reads, verifies and parses the cookie.
+
+    Args:
+      cookie_name: The cookie to get.
+
+    Returns:
+      a dict containing user, is_admin, and is_author if the cookie
+      is present and valid. Otherwise, None.
+    """
+    cookies = self.headers.get('Cookie')
+    if isinstance(cookies, str):
+      for c in cookies.split(';'):
+        matched_cookie = self._MatchCookie(cookie_name, c)
+        if matched_cookie:
+          return self._ParseCookie(matched_cookie)
+    return self.NULL_COOKIE
+
+  def _MatchCookie(self, cookie_name, cookie):
+    """Matches the cookie.
+
+    Args:
+      cookie_name: The name of the cookie.
+      cookie: The full cookie (name=value).
+
+    Returns:
+      The cookie if it matches or None if it doesn't match.
+    """
+    try:
+      (cn, cd) = cookie.strip().split('=', 1)
+      if cn != cookie_name:
+        return None
+    except (IndexError, ValueError):
+      return None
+    return cd
+
+  def _ParseCookie(self, cookie):
+    """Parses the cookie and returns NULL_COOKIE if it's invalid.
+
+    Args:
+      cookie: The text of the cookie.
+
+    Returns:
+      A map containing the values in the cookie.
+    """
+    try:
+      (hashed, cookie_data) = cookie.split('|', 1)
+      # global cookie_secret
+      if hashed != str(hash(cookie_secret + cookie_data) & 0x7FFFFFF):
+        return self.NULL_COOKIE
+      values = cookie_data.split('|')
+      return {
+          COOKIE_UID: values[0],
+          COOKIE_ADMIN: values[1] == 'admin',
+          COOKIE_AUTHOR: values[2] == 'author',
+      }
+    except (IndexError, ValueError):
+      return self.NULL_COOKIE
+
+  def _DoReset(self, cookie, specials, params):  # debug only; resets this db
+    """Handles the /reset url for administrators to reset the database.
+
+    Args:
+      cookie: The cookie for this request. (unused)
+      specials: Other special values for this request. (unused)
+      params: Cgi parameters. (unused)
+    """
+    self._ResetDatabase()
+    self._SendTextResponse('Server reset to default values...', None)
+
+  def _DoUpload2(self, cookie, specials, params):
+    """Handles the /upload2 url: finish the upload and save the file.
+
+    Args:
+      cookie: The cookie for this request.
+      specials: Other special values for this request.
+      params: Cgi parameters. (unused)
+    """
+    (filename, file_data) = self._ExtractFileFromRequest()
+    directory = self._MakeUserDirectory(cookie[COOKIE_UID])
+
+    message = None
+    url = None
+    try:
+      f = _Open(directory, filename, 'wb')
+      f.write(file_data)
+      f.close()
+      (host, port) = http_server.server_address
+      url = 'http://%s:%d/%s/%s/%s' % (
+          host, port, specials[SPECIAL_UNIQUE_ID], cookie[COOKIE_UID], filename)
+    except IOError, ex:
+      message = 'Couldn\'t write file %s: %s' % (filename, ex.message)
+      _Log(message)
+
+    specials['_message'] = message
+    self._SendTemplateResponse(
+        '/upload2.gtl', specials,
+        {'url': url})
+
+  def _ExtractFileFromRequest(self):
+    """Extracts the file from an upload request.
+
+    Returns:
+      (filename, file_data)
+    """
+    form = cgi.FieldStorage(
+        fp=self.rfile,
+        headers=self.headers,
+        environ={'REQUEST_METHOD': 'POST',
+                 'CONTENT_TYPE': self.headers.getheader('content-type')})
+
+    upload_file = form['upload_file']
+    file_data = upload_file.file.read()
+    return (upload_file.filename, file_data)
+
+  def _MakeUserDirectory(self, uid):
+    """Creates a separate directory for each user to avoid upload conflicts.
+
+    Args:
+      uid: The user to create a directory for.
+
+    Returns:
+      The new directory path (/uid/).
+    """
+
+    directory = RESOURCE_PATH + os.sep + str(uid) + os.sep
+    try:
+      print 'mkdir: ', directory
+      os.mkdir(directory)
+      # throws an exception if directory already exists,
+      # however exception type varies by platform
+    except Exception:
+      pass  # just ignore it if it already exists
+    return directory
+
+  def _SendRedirect(self, url, unique_id):
+    """Sends a 302 redirect.
+
+    Automatically adds the unique_id.
+
+    Args:
+      url: The location to redirect to which must start with '/'.
+      unique_id: The unique id to include in the url.
+    """
+    if not url:
+      url = '/'
+    url = '/' + unique_id + url
+    self.send_response(302)
+    self.send_header('Location', url)
+    self.send_header('Pragma', 'no-cache')
+    self.send_header('Content-type', 'text/html')
+    self.send_header('X-XSS-Protection', '0')
+    self.end_headers()
+    self.wfile.write(
+        '''<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML//EN'>
+        <html><body>
+        <title>302 Redirect</title>
+        Redirected <a href="%s">here</a>
+        </body></html>'''
+        % (url,))
+
+  def _GetHandlerFunction(self, path):
+    try:
+      return getattr(GruyereRequestHandler, '_Do' + path[1:].capitalize())
+    except AttributeError:
+      return None
+
+  def do_POST(self):  # part of BaseHTTPRequestHandler interface
+    self.DoGetOrPost()
+
+  def do_GET(self):  # part of BaseHTTPRequestHandler interface
+    self.DoGetOrPost()
+
+  def DoGetOrPost(self):
+    """Validate an http get or post request and call HandleRequest."""
+
+    url = urlparse(self.path)
+    path = url[2]
+    query = url[4]
+
+    # Normally, Gruyere only accepts connections to/from localhost. If you
+    # would like to allow access from other ip addresses, add the addresses
+    # of the other machines to allowed_ips and change insecure_mode to True
+    # above. This makes the application more vulnerable to a real attack so
+    # you should only add ips of machines you completely control and make
+    # sure that you are not using them to access any other web pages while
+    # you are using Gruyere.
+
+    allowed_ips = ['127.0.0.1']
+
+    # WARNING! DO NOT CHANGE THE FOLLOWING SECTION OF CODE!
+
+    # This application is very exploitable. See main for details. What we're
+    # doing here is (2) and (3) on the previous list:
+    #   (2) If a request is received from any IP other than localhost, quit.
+    # An external attacker could still mount an attack on this IP by putting
+    # an attack on an external web page, e.g., a web page that redirects to
+    # a vulnerable url on 127.0.0.1 (which is why we use a random number).
+    #   (3) Inject a random identifier as the first part of the path and
+    # quit if a request is received without this identifier (except for an
+    # empty path which redirects and /favicon.ico).
+
+    request_ip = self.client_address[0]                      # DO NOT CHANGE
+    if request_ip not in allowed_ips:                        # DO NOT CHANGE
+      print >>sys.stderr, (                                  # DO NOT CHANGE
+          'DANGER! Request from bad ip: ' + request_ip)      # DO NOT CHANGE
+      _Exit('bad_ip')                                        # DO NOT CHANGE
+
+    if (server_unique_id not in path                         # DO NOT CHANGE
+        and path != '/favicon.ico'):                         # DO NOT CHANGE
+      if path == '' or path == '/':                          # DO NOT CHANGE
+        self._SendRedirect('/', server_unique_id)            # DO NOT CHANGE
+        return                                               # DO NOT CHANGE
+      else:                                                  # DO NOT CHANGE
+        print >>sys.stderr, (                                # DO NOT CHANGE
+            'DANGER! Request without unique id: ' + path)    # DO NOT CHANGE
+        _Exit('bad_id')                                      # DO NOT CHANGE
+
+    path = path.replace('/' + server_unique_id, '', 1)       # DO NOT CHANGE
+
+    # END WARNING!
+
+    self.HandleRequest(path, query, server_unique_id)
+
+  def HandleRequest(self, path, query, unique_id):
+    """Handles an http request.
+
+    Args:
+      path: The path part of the url, with leading slash.
+      query: The query part of the url, without leading question mark.
+      unique_id: The unique id from the url.
+    """
+
+    path = urllib.unquote(path)
+
+    if not path:
+      self._SendRedirect('/', server_unique_id)
+      return
+    params = cgi.parse_qs(query)  # url.query
+    specials = {}
+    cookie = self._GetCookie('GRUYERE')
+    database = self._GetDatabase()
+    specials[SPECIAL_COOKIE] = cookie
+    specials[SPECIAL_DB] = database
+    specials[SPECIAL_PROFILE] = database.get(cookie.get(COOKIE_UID))
+    specials[SPECIAL_PARAMS] = params
+    specials[SPECIAL_UNIQUE_ID] = unique_id
+
+    if path in self._PROTECTED_URLS and not cookie[COOKIE_ADMIN]:
+      self._SendError('Invalid request', cookie, specials, params)
+      return
+
+    try:
+      handler = self._GetHandlerFunction(path)
+      if callable(handler):
+        (handler)(self, cookie, specials, params)
+      else:
+        try:
+          self._SendFileResponse(path, cookie, specials, params)
+        except IOError:
+          self._DoBadUrl(path, cookie, specials, params)
+    except KeyboardInterrupt:
+      _Exit('KeyboardInterrupt')
+
+
+def _Log(message):
+  print >>sys.stderr, message
+
+
+if __name__ == '__main__':
+  main()
diff --git a/modified_gruyere_code/snippets.gtl b/modified_gruyere_code/snippets.gtl
new file mode 100644
index 0000000000000000000000000000000000000000..e4b936a3725afbe4fcb226895f5cc0359b8aa0aa
--- /dev/null
+++ b/modified_gruyere_code/snippets.gtl
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<!-- Copyright 2017 Google Inc. -->
+<html>
+<head>
+<title>Gruyere: Snippets</title>
+[[include:base.css]][[/include:base.css]]
+<script src="/{{_unique_id}}/lib.js" text="text/javascript">
+</script>
+</head>
+
+<body>
+{{# With a valid uid parameter this renders another user's snippets.
+  # We detect that case by checking if:_db.*uid. Without a uid specified,
+  # it renders the logged in user's snippets and includes links to delete
+  # individual snippets.
+}}
+[[include:menubar.gtl]][[/include:menubar.gtl]]
+<div>
+<h2 class='has-refresh' id="user_name">
+[[if:uid]]
+  [[if:_db.*uid]]{{_db.*uid.name:text}}[[/if:_db.*uid]][[if:!_db.*uid]]{{uid.0}}[[/if:!_db.*uid]]
+  [[if:_db.*uid.icon]]<img alt='' height='32' width='32' src='{{_db.*uid.icon:text}}'>[[/if:_db.*uid.icon]]
+[[/if:uid]]
+[[if:!uid]]
+  My Snippets
+  [[if:_profile.icon]]<img alt='' height='32' width='32' src='{{_profile.icon:text}}'>[[/if:_profile.icon]]
+[[/if:!uid]]
+</h2>
+<div class='refresh'><a class='button'
+  onclick='_refreshSnippets("{{_unique_id}}", "[[if:uid]]{{uid.0}}[[/if:uid]][[if:!uid]]{{_cookie.uid}}[[/if:!uid]]")'
+  href='#'>Refresh</a></div>
+<div class='content'>
+{{# Someone else's snippets}}
+[[if:uid]]
+  [[if:!_db.*uid.is_author]]
+    [[if:_db.*uid]]{{_db.*uid.name:text}}[[/if:_db.*uid]]
+    The requested author is unknown.
+  [[/if:!_db.*uid.is_author]]
+  [[if:_db.*uid.is_author]]
+    [[if:!_db.*uid.snippets.0]]
+      No snippets.
+    [[/if:!_db.*uid.snippets.0]]
+    [[if:_db.*uid.snippets.0]]
+      <table>
+        <tr><td colspan='2'><b>All snippets:</b></td></tr>
+        [[for:_db.*uid.snippets]]
+          <tr>
+            <td valign='top'>
+              <script>document.write({{_key}} + 1)</script>&nbsp;&nbsp;
+            </td>
+            <td valign='top'>
+              <div id='{{_key}}'>
+                {{_this:html}}
+              </div>
+            </td>
+          </tr>
+        [[/for:_db.*uid.snippets]]
+      </table>
+      <br>
+      <a href='{{_db.*uid.web_site:text}}'>[[if:_db.*uid]]{{_db.*uid.name:text}}[[/if:_db.*uid]][[if:!_db.*uid]]{{uid.0}}[[/if:!_db.*uid]]'s site</a>
+    [[/if:_db.*uid.snippets.0]]
+  [[/if:_db.*uid.is_author]]
+[[/if:uid]]
+{{# Your snippets }}
+[[if:!uid]]
+  [[if:!_profile.is_author]]
+    You are not an author.
+  [[/if:!_profile.is_author]]
+  [[if:_profile.is_author]]
+    [[if:!_profile.snippets.0]]
+      No snippets.
+    [[/if:!_profile.snippets.0]]
+    [[if:_profile.snippets.0]]
+      <br>
+      <table>
+        <tr><td colspan='2'><b>All snippets:</b></td></tr>
+        [[for:_profile.snippets]]
+          <tr>
+            <td valign='top'>
+              <script>document.write({{_key}} + 1)</script>&nbsp;&nbsp;
+            </td>
+            <td valign='top'>
+              <a href='/{{_unique_id}}/deletesnippet?index={{_key}}'>[X]</a>&nbsp;
+            </td>
+            <td valign='top'>
+              <div id='{{_key}}'>
+                {{_this:html}}
+              </div>
+            </td>
+          </tr>
+        [[/for:_profile.snippets]]
+      </table>
+    [[/if:_profile.snippets.0]]
+  [[/if:_profile.is_author]]
+  <br>
+  <a href='{{_profile.web_site:text}}'>My site</a>
+[[/if:!uid]]
+</div>
+</body>
+</html>