I am currently studying about Client Side Web Security
and I have this task regarding Cross-Site Scripting. I am stuck for the past two days and don’t know where to start, I would appreciate if someone would be able to provide some tipps and guidance on how to solve/approach the task 🙂
The admin of https://noncense.is.hackthe.space regurarily debunks common misconceptions on his website and is very concerned with the security of his page. He also gives his readers a possibility to report errors within the page. If you encounter any errors, please do not hesitate to report them!
The flag is hidden in the session cookie of the admin.
The admin only accepts URLs that start with https://noncense.is.hackthe.space/ and their firewall prevents any connection to the Internet.
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import time, base64, re, json
from database import DBConnection
import logging, os
import redis
PORT = 5000
URL = os.environ['EXTERNAL_URL']
db = DBConnection()
QUEUE = 'help-urls'
rq = redis.Redis(host=os.environ['REDIS_HOST'], decode_responses=True)
def create_admin():
if db.find_by_name("admin") == None:
db.insert("admin", os.environ["ADMIN_PW"], os.environ["ADMIN_SESSION"])
def get_session_id(headers) -> (str | None):
cookies = None if headers["Cookie"] is None else (
dict(i.split('=', 1) for i in headers["Cookie"].split('; ')))
return cookies.get("sessionID", None) if cookies else None
def generate_totally_secure_nonce() -> bytes:
seconds = int(time.time())
print(f"Generated nonce for {seconds}")
return base64.b64encode(bytes(str(seconds), "utf-8"))
def validate(txt: str):
regex = r"^[a-zA-Z0-9_{}]{0,100}$"
return re.match(regex, txt)
def is_admin(headers):
# Check if cookies contain the correct value (1010) for "sessionID" cookie
cookies = None if headers["Cookie"] is None else (
dict(i.split('=', 1) for i in headers["Cookie"].split('; ')))
return cookies is not None and cookies.get("sessionID", "") == db.find_by_name("admin")["sessionID"]
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
serve_directly = ["/login.html", "/register.html", "/index.html"]
resource = self.path
# remove query parameters and fragements from path
to_truncate = ["?", "#"]
for c in to_truncate:
occurrence = resource.find(c)
if occurrence != -1:
resource = resource[:occurrence]
if resource == "/":
resource = "/index.html"
# serve the correct file
if resource.startswith('/users/'):
user_id = self.path[7:]
try:
user = db.find_by_id(user_id)
except:
self.serve_file("error.html", 404)
return
# Check permissions
_sessionId = get_session_id(self.headers)
if not user or not _sessionId or not db.authenticate(user_id, _sessionId):
self.serve_file("error.html", 404)
return
# Serve profile page
self.serve_file("profile.html", 200, {"[USER_PLACEHOLDER]": user["username"], "[DESC_PLACEHOLDER]": user["description"] if user["description"] else "No description"})
elif resource.startswith('/help.html'):
queue_size = str(rq.llen(QUEUE))
self.serve_file('help.html', 200, {"[QUEUE_LOAD_PLACEHOLDER]": queue_size})
elif resource in serve_directly:
self.serve_file(resource.replace("/", ""), 200)
elif resource == "/script.js":
self.serve_file("script.js", 200, content_type="text/javascript")
elif resource == "/style.css":
self.serve_file("style.css", 200, content_type="text/css")
elif resource == "/whoAmI":
cookie = get_session_id(self.headers)
if not cookie:
self.send(404, "404 Not Found")
return
user = db.find_by_cookie(cookie)
if not user:
self.send(404, "404 Not Found")
return
self.send(200, json.dumps({"id": str(user["_id"])}))
else:
self.serve_file("error.html", 404)
def serve_file(self, file: str, err_code: int, placeholders: dict = dict(), content_type = None):
nonce = generate_totally_secure_nonce()
self.send_response(err_code)
self.send_header("Content-Security-Policy", f"default-src 'self' 'nonce-{nonce.decode('utf-8')}'; style-src-elem 'self' fonts.googleapis.com cdn.jsdelivr.net 'nonce-{nonce.decode('utf-8')}'; img-src http.cat; font-src fonts.gstatic.com")
if content_type is None:
self.send_header('Content-type', 'text/html')
else:
self.send_header('Content-type', content_type)
self.end_headers()
with open("resources/" + file, "rb") as file:
content = file.read()
content = content.replace(b"[NONCEPLACEHOLDER]", nonce)
for key, value in placeholders.items():
content = content.replace(bytes(key, "utf-8"), bytes(value, "utf-8"))
self.wfile.write(content)
def do_POST(self):
content_len = int(self.headers.get('Content-Length'))
body = self.rfile.read(content_len).decode("utf-8")
print(f"Received {body=}")
if self.path.startswith("/users/"):
user_id = self.path[7:]
if not user_id:
self.send(404, "404 Not Found")
return
# Validate description if user is not admin
if not validate(body):
self.send(422, "422 Unprocessable Entity")
return
session_cookie = get_session_id(self.headers)
if not session_cookie:
self.send(403, "403 Forbidden")
return
_is_admin = db.is_admin(session_cookie)
is_owner = db.authenticate(user_id, session_cookie)
if (_is_admin and is_owner) or
(not _is_admin and not is_owner):
self.send(403, "403 Forbidden")
return
db.update_description(user_id, body)
self.send(200, "200 Ok")
elif self.path == "/register":
# Validate body and check if both necessary fields are set
try:
user_data = json.loads(body)
except:
self.send(422, "422 Unprocessable Entity")
return
if (not user_data) or user_data.get("username", "") == ""
or user_data.get("password", "") == ""
or not validate(user_data['username'])
or not validate(user_data['password']):
self.send(422, "422 Unprocessable Entity")
return
if db.find_by_name(user_data['username']) is not None:
self.send(409, "409 Conflict")
return
res = db.insert(user_data['username'], user_data['password'])
user = db.find_by_id(res.inserted_id)
self.send(200, json.dumps({"id": str(user["_id"]), "username": user["username"]}), user['sessionID'])
elif self.path == "/login":
# Validate body and check if both necessary fields are set
try:
user_data = json.loads(body)
except:
self.send(422, "422 Unprocessable Entity")
return
if (not user_data) or user_data.get("username", "") == "" or user_data.get("password", "") == "":
self.send(422, "422 Unprocessable Entity")
return
user_cookie = db.get_session_cookie(user_data['username'], user_data['password'])
if user_cookie:
user_id = str(db.find_by_name(user_data['username'])['_id'])
self.send(200, json.dumps({"id": user_id}), user_cookie)
else:
self.send(401, "401 Unauthorized")
elif self.path == "/help":
try:
user_data = json.loads(body)
except:
self.send(422, "422 Unprocessable Entity")
return
if not user_data.get('url', False) or not user_data['url'].startswith(URL):
self.send(422, "422 Unprocessable Entity")
return
try:
rq.rpush(QUEUE, user_data['url'])
except Exception as e:
print(f"Something went wrong: f{e=}")
self.send(500, "500 Internal Server Error")
return
self.send(200, "200 Ok")
else:
# Send default error response
self.send(404, "404 Not Found")
def send(self, code: int, msg: str, response_cookie: str = None, _json: bool = False):
self.send_response(code)
if (_json):
self.send_header('Content-type', 'application/json')
else:
self.send_header('Content-type', 'text/html')
if response_cookie:
self.send_header('Set-Cookie', f"sessionID={response_cookie}")
self.end_headers()
self.wfile.write(bytes(msg, "utf-8"));
def run(server_class=ThreadingHTTPServer, handler_class=SimpleHTTPRequestHandler, port=PORT):
server_address = ('', port)
httpd = server_class(server_address, handler_class)
print(f'Starting server on port {port}...')
httpd.serve_forever()
if __name__ == '__main__':
logging.info("creating admin user...")
create_admin()
run()