├── .github └── workflows │ └── test.yaml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.py ├── config.py ├── core ├── __init__.py ├── decorators.py ├── directives.py ├── helpers.py ├── middleware.py ├── models.py ├── parser.py ├── security.py ├── view_override.py └── views.py ├── db ├── __init__.py ├── agents.py ├── content.py ├── owners.py ├── solutions.py └── titles.py ├── requirements.txt ├── setup.py ├── static ├── all.css ├── bootstrap │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map ├── extra.css ├── favicon.ico ├── images │ └── dvgql_logo.png ├── jquery │ ├── graphql.js │ ├── jquery.js │ ├── jquery.min.js │ ├── jquery.min.map │ ├── jquery.slim.js │ ├── jquery.slim.min.js │ └── jquery.slim.min.map ├── screenshots │ ├── create.png │ ├── index.png │ ├── pastes.png │ └── solution.png ├── simple-sidebar.css └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── templates ├── about.html ├── audit.html ├── index.html ├── partials │ ├── base.html │ ├── base_header.html │ ├── base_scripts.html │ ├── navbar.html │ ├── pastes │ │ ├── create_paste.html │ │ ├── import_paste.html │ │ ├── my_pastes.html │ │ ├── public_pastes.html │ │ └── upload_paste.html │ ├── sidebar.html │ └── solutions │ │ ├── solution_1.html │ │ ├── solution_10.html │ │ ├── solution_11.html │ │ ├── solution_12.html │ │ ├── solution_13.html │ │ ├── solution_14.html │ │ ├── solution_15.html │ │ ├── solution_16.html │ │ ├── solution_17.html │ │ ├── solution_18.html │ │ ├── solution_19.html │ │ ├── solution_2.html │ │ ├── solution_20.html │ │ ├── solution_21.html │ │ ├── solution_22.html │ │ ├── solution_3.html │ │ ├── solution_4.html │ │ ├── solution_5.html │ │ ├── solution_6.html │ │ ├── solution_7.html │ │ ├── solution_8.html │ │ └── solution_9.html ├── paste.html └── solutions.html ├── tests ├── common.py ├── test_args_and_directives.py ├── test_auth.py ├── test_batching.py ├── test_cookies.py ├── test_graphiql.py ├── test_graphql.py ├── test_intialize.py ├── test_introspect.py ├── test_mode.py ├── test_mutations.py ├── test_queries.py ├── test_rollback.py ├── test_vulnerabilities.py └── test_websockets.py └── version.py /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - blackhatgraphql 8 | 9 | push: 10 | branches: 11 | - master 12 | - blackhatgraphql 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - ubuntu-22.04 21 | - ubuntu-20.04 22 | pyver: 23 | - '3.6' 24 | - '3.7' 25 | - '3.8' 26 | - '3.9' 27 | - '3.10' 28 | exclude: 29 | - os: ubuntu-22.04 30 | pyver: 3.6 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-python@v4 35 | with: 36 | python-version: ${{ matrix.pyver }} 37 | cache: 'pip' 38 | - run: pip install -r requirements.txt 39 | 40 | - name: Run DVGA 41 | run: | 42 | nohup python3 ./app.py & 43 | 44 | - name: Wait for server (sleep 5 secs) 45 | run: | 46 | sleep 5 47 | 48 | - name: Run DVGA Tests 49 | run: | 50 | python3 -m pytest tests/* 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .DS_Store 141 | dvga.db 142 | tools/ 143 | 144 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | LABEL description="Damn Vulnerable GraphQL Application" 4 | LABEL github="https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application" 5 | LABEL maintainers="Dolev Farhi & Nick Aleks" 6 | 7 | ARG TARGET_FOLDER=/opt/dvga 8 | WORKDIR $TARGET_FOLDER/ 9 | 10 | RUN apt install curl git 11 | 12 | RUN useradd dvga -m 13 | RUN chown dvga. $TARGET_FOLDER/ 14 | USER dvga 15 | 16 | RUN python -m venv venv 17 | RUN . venv/bin/activate 18 | RUN pip3 install --user --upgrade pip --no-warn-script-location --disable-pip-version-check 19 | 20 | ADD --chown=dvga:dvga core /opt/dvga/core 21 | ADD --chown=dvga:dvga db /opt/dvga/db 22 | ADD --chown=dvga:dvga static /opt/dvga/static 23 | ADD --chown=dvga:dvga templates /opt/dvga/templates 24 | 25 | COPY --chown=dvga:dvga app.py /opt/dvga 26 | COPY --chown=dvga:dvga config.py /opt/dvga 27 | COPY --chown=dvga:dvga setup.py /opt/dvga/ 28 | COPY --chown=dvga:dvga version.py /opt/dvga/ 29 | COPY --chown=dvga:dvga requirements.txt /opt/dvga/ 30 | 31 | RUN pip3 install -r requirements.txt --user --no-warn-script-location 32 | RUN python setup.py 33 | 34 | EXPOSE 5013/tcp 35 | CMD ["python", "app.py"] 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Exposed Atoms 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Damn Vulnerable GraphQL Application 2 | 3 | Damn Vulnerable GraphQL Application is an intentionally vulnerable implementation of Facebook's GraphQL technology, to learn and practice GraphQL Security. 4 | 5 |

6 | DVGA 7 |

8 | 9 | # Table of Contents 10 | * [About DVGA](#about) 11 | * [Operation Modes](#operation-modes) 12 | * [Scenarios](#scenarios) 13 | * [Prerequisites](#prerequisites) 14 | * [Installation](#installation) 15 | * [Installation - Docker](#docker) 16 | * [Installation - Docker Registry](#docker-registry) 17 | * [Installation - Server](#server) 18 | * [Screenshots](#screenshots) 19 | * [Maintainers](#maintainers) 20 | * [Contributors](#contributors) 21 | * [Mentions](#mentions) 22 | * [Disclaimer](#disclaimer) 23 | * [License](#license) 24 | 25 | # About DVGA 26 | 27 | Damn Vulnerable GraphQL is a deliberately weak and insecure implementation of GraphQL that provides a safe environment to attack a GraphQL application, allowing developers and IT professionals to test for vulnerabilities. 28 | 29 | ## DVGA Operation Support 30 | 31 | - Queries 32 | - Mutations 33 | - Subscriptions 34 | 35 | DVGA has numerous flaws, such as Injections, Code Executions, Bypasses, Denial of Service, and more. See the full list under the [Scenarios](#scenarios) section. A public [Postman collection](https://www.postman.com/devrel/workspace/ab3d0551-b65d-4588-b464-1a317e8d7e98/collection/14270212-b5875c90-d36e-43f4-8bd7-2c81b556245d?action=share&creator=14270212) is also available to replay solutions to the challenges. You can import the collection by clicking on the Run in Postman button below. 36 | 37 | [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/14270212-b5875c90-d36e-43f4-8bd7-2c81b556245d?action=collection%2Ffork&collection-url=entityId%3D14270212-b5875c90-d36e-43f4-8bd7-2c81b556245d%26entityType%3Dcollection%26workspaceId%3Dab3d0551-b65d-4588-b464-1a317e8d7e98) 38 | 39 | # Operation Modes 40 | 41 | DVGA supports Beginner and Expert level game modes, which will change the exploitation difficulty. 42 | 43 | # Scenarios 44 | 45 | * **Reconnaissance** 46 | * Discovering GraphQL 47 | * Fingerprinting GraphQL 48 | * **Denial of Service** 49 | * Batch Query Attack 50 | * Deep Recursion Query Attack 51 | * Resource Intensive Query Attack 52 | * Field Duplication Attack 53 | * Aliases based Attack 54 | * **Information Disclosure** 55 | * GraphQL Introspection 56 | * GraphiQL Interface 57 | * GraphQL Field Suggestions 58 | * Server Side Request Forgery 59 | * Stack Trace Errors 60 | * **Code Execution** 61 | * OS Command Injection #1 62 | * OS Command Injection #2 63 | * **Injection** 64 | * Stored Cross Site Scripting 65 | * Log spoofing / Log Injection 66 | * HTML Injection 67 | * SQL Injection 68 | * **Authorization Bypass** 69 | * GraphQL JWT Token Forge 70 | * GraphQL Interface Protection Bypass 71 | * GraphQL Query Deny List Bypass 72 | * **Miscellaneous** 73 | * GraphQL Query Weak Password Protection 74 | * Arbitrary File Write // Path Traversal 75 | 76 | # Prerequisites 77 | 78 | The following Python3 libraries are required: 79 | 80 | * Python3 (3.6 - 3.10) 81 | * Flask 82 | * Flask-SQLAlchemy 83 | * Flask-Sockets 84 | * Gevent 85 | * Graphene 86 | * Graphene-SQLAlchemy 87 | * Rx 88 | 89 | See [requirements.txt](requirements.txt) for dependencies. 90 | 91 | 92 | # Installation 93 | 94 | ## Docker 95 | 96 | ### Clone the repository 97 | 98 | `git clone https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application.git && cd Damn-Vulnerable-GraphQL-Application` 99 | 100 | ### Build the Docker image 101 | 102 | `docker build -t dvga .` 103 | 104 | ### Create a container from the image 105 | 106 | `docker run -d -t -p 5013:5013 -e WEB_HOST=0.0.0.0 --name dvga dvga` 107 | 108 | In your browser, navigate to http://localhost:5013 109 | 110 | Note: if you need the application to bind on a specific port (e.g. 8080), use **-e WEB_PORT=8080**. 111 | 112 | ## Docker Registry 113 | 114 | ### Pull the docker image from Docker Hub 115 | 116 | `docker pull dolevf/dvga` 117 | 118 | Docker Hub image: [dolevf/dvga](https://hub.docker.com/r/dolevf/dvga) 119 | 120 | ### Create a container from the image 121 | 122 | `docker run -t -p 5013:5013 -e WEB_HOST=0.0.0.0 dolevf/dvga` 123 | 124 | In your browser, navigate to http://localhost:5013 125 | 126 | ## Server 127 | 128 | ### Navigate to /opt 129 | 130 | `cd /opt/` 131 | 132 | ### Clone the repository 133 | 134 | `git clone git@github.com:dolevf/Damn-Vulnerable-GraphQL-Application.git && cd Damn-Vulnerable-GraphQL-Application` 135 | 136 | ### Install Requirements 137 | 138 | `pip3 install -r requirements.txt` 139 | 140 | ### Run application 141 | 142 | `python3 app.py` 143 | 144 | In your browser, navigate to http://localhost:5013. 145 | 146 | # Screenshots 147 | 148 | ![DVGA](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/master/static/screenshots/index.png) 149 | ![DVGA](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/master/static/screenshots/solution.png) 150 | ![DVGA](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/master/static/screenshots/pastes.png) 151 | ![DVGA](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/master/static/screenshots/create.png) 152 | 153 | # Maintainers 154 | 155 | * [Dolev Farhi](https://github.com/dolevf) 156 | * [Connor McKinnon](https://github.com/connormckinnon93) 157 | * [Nick Aleks](https://github.com/nicholasaleks) 158 | # Contributors 159 | A big Thank You to the kind people who helped make DVGA better: 160 | * [Halfluke](https://github.com/halfluke) 161 | 162 | # Mentions 163 | * [Black Hat GraphQL - No Starch Press](https://blackhatgraphql.com) 164 | * [OWASP Vulnerable Web Applications Directory](https://owasp.org/www-project-vulnerable-web-applications-directory/) 165 | * [GraphQL Weekly](https://www.graphqlweekly.com/issues/221/#content) 166 | * [DZone API Security Weekly](https://dzone.com/articles/api-security-weekly-issue-121) 167 | * [KitPloit](https://www.kitploit.com/2021/02/damn-vulnerable-graphql-application.html) 168 | * [tl;dr sec #72](https://tldrsec.com/blog/tldr-sec-072/) 169 | * [Intigriti Blog](https://blog.intigriti.com/2021/02/17/bug-bytes-110-scope-based-recon-finding-more-idors-how-to-hack-sharepoint/) 170 | * [STÖK - Bounty Thursdays #26](https://www.youtube.com/watch?v=645Tb7ySQFk) 171 | * [Brakeing Security 2021-007](https://brakeingsecurity.com/2021-007-news-google-asking-for-oss-to-embrace-standards-insider-threat-at-yandex-vectr-discussion) 172 | * [Yes We Hack - How to Exploit GraphQL](https://blog.yeswehack.com/yeswerhackers/how-exploit-graphql-endpoint-bug-bounty/) 173 | * [GraphQL Editor](https://blog.graphqleditor.com/dvga) 174 | * [GraphQL Hacking (Portuguese)](https://www.youtube.com/watch?v=4gXOerUZ7fw) 175 | * [InQL GraphQL Scanner Demo](https://www.youtube.com/watch?v=KOCBeJmTs78) 176 | * [H4ck3d - Security Conference 2021 (Spanish)](https://youtu.be/hg_kVoy-W1s) 177 | * [Christina Hasternath - GraphQLConf 2021](https://www.youtube.com/watch?v=tPO1jl0tCKg) 178 | * [Hacking APIs (Ch14) by Corey Ball - No Starch Press](https://nostarch.com/hacking-apis) 179 | * [Hacking Simplified Part #1](https://www.youtube.com/watch?v=w0QOAacuPgQ) 180 | * [Hacking Simplified Part #2](https://www.youtube.com/watch?v=YA-mL9Z8SNI) 181 | * [Hacking Simplified Part #3](https://www.youtube.com/watch?v=kUTIFx8vGQs) 182 | 183 | # Disclaimer 184 | 185 | DVGA is highly insecure, and as such, should not be deployed on internet facing servers. By default, the application is listening on 127.0.0.1 to avoid misconfigurations. 186 | 187 | DVGA is intentionally flawed and vulnerable, as such, it comes with no warranties. By using DVGA, you take full responsibility for using it. 188 | 189 | # License 190 | 191 | It is distributed under the MIT License. See LICENSE for more information. 192 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import config 2 | import sys 3 | import os 4 | 5 | from flask import Flask 6 | from flask_sqlalchemy import SQLAlchemy 7 | from flask_graphql_auth import GraphQLAuth 8 | 9 | app = Flask(__name__, static_folder="static/") 10 | app.secret_key = os.urandom(24) 11 | app.config["SQLALCHEMY_DATABASE_URI"] = config.SQLALCHEMY_DATABASE_URI 12 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = config.SQLALCHEMY_TRACK_MODIFICATIONS 13 | app.config["UPLOAD_FOLDER"] = config.WEB_UPLOADDIR 14 | app.config['SECRET_KEY'] = 'dvga' 15 | app.config["JWT_SECRET_KEY"] = 'dvga' 16 | app.config["JWT_ACCESS_TOKEN_EXPIRES"] = 120 17 | app.config["JWT_REFRESH_TOKEN_EXPIRES"] = 30 18 | 19 | auth = GraphQLAuth(app) 20 | 21 | app.app_protocol = lambda environ_path_info: 'graphql-ws' 22 | 23 | db = SQLAlchemy(app) 24 | 25 | if __name__ == '__main__': 26 | sys.setrecursionlimit(100000) 27 | 28 | os.popen("python3 setup.py").read() 29 | 30 | from core.views import * 31 | from gevent import pywsgi 32 | from geventwebsocket.handler import WebSocketHandler 33 | from version import VERSION 34 | 35 | server = pywsgi.WSGIServer((config.WEB_HOST, int(config.WEB_PORT)), app, handler_class=WebSocketHandler) 36 | print("DVGA Server Version: {version} Running...".format(version=VERSION)) 37 | server.serve_forever() -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # SQLAlchemy 4 | SQLALCHEMY_FILE = f"{os.getcwd()}/dvga.db" 5 | SQLALCHEMY_DATABASE_URI = f"sqlite:///{SQLALCHEMY_FILE}" 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | 8 | # Flask 9 | WEB_HOST = os.environ.get('WEB_HOST', '127.0.0.1') 10 | WEB_PORT = os.environ.get('WEB_PORT', 5013) 11 | WEB_DEBUG = os.environ.get('WEB_DEBUG', True) 12 | WEB_UPLOADDIR = 'pastes/' 13 | 14 | # GraphQL Security Protection 15 | MAX_DEPTH = 8 16 | MAX_COST = 10 -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/decorators.py: -------------------------------------------------------------------------------- 1 | # graphql-utilities library 2 | from functools import wraps 3 | 4 | def run_only_once(resolve_func): 5 | @wraps(resolve_func) 6 | def wrapper(self, next, root, info, *args, **kwargs): 7 | has_context = info.context is not None 8 | decorator_name = "__{}_run__".format(self.__class__.__name__) 9 | 10 | if has_context: 11 | if isinstance(info.context, dict) and not info.context.get(decorator_name, False): 12 | info.context[decorator_name] = True 13 | return resolve_func(self, next, root, info, *args, **kwargs) 14 | elif not isinstance(info.context, dict) and not getattr(info.context, decorator_name, False): 15 | setattr(info.context, decorator_name, True) 16 | return resolve_func(self, next, root, info, *args, **kwargs) 17 | 18 | return next(root, info, *args, **kwargs) 19 | 20 | return wrapper 21 | -------------------------------------------------------------------------------- /core/directives.py: -------------------------------------------------------------------------------- 1 | from unicodedata import name 2 | from graphql import GraphQLArgument, GraphQLNonNull, GraphQLString 3 | from graphql.type.directives import GraphQLDirective, DirectiveLocation, GraphQLDeprecatedDirective, GraphQLSkipDirective 4 | 5 | ShowNetworkDirective = GraphQLDirective( 6 | name="show_network", 7 | locations=[ 8 | DirectiveLocation.FIELD, 9 | DirectiveLocation.FRAGMENT_SPREAD, 10 | DirectiveLocation.INLINE_FRAGMENT, 11 | ], 12 | args={ 13 | "style": GraphQLArgument( 14 | GraphQLNonNull(GraphQLString) 15 | ) 16 | }, 17 | description="Displays the network associated with an IP Address (CIDR or Net)." 18 | ) 19 | 20 | DeprecatedDirective = GraphQLDeprecatedDirective 21 | SkipDirective = GraphQLSkipDirective -------------------------------------------------------------------------------- /core/helpers.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import uuid 3 | import os 4 | from config import WEB_UPLOADDIR 5 | from jwt import decode 6 | from core.models import ServerMode 7 | 8 | def run_cmd(cmd): 9 | return os.popen(cmd).read() 10 | 11 | def initialize(): 12 | return run_cmd('python3 setup.py') 13 | 14 | def generate_uuid(): 15 | return str(uuid.uuid4())[0:6] 16 | 17 | def decode_base64(text): 18 | return base64.b64decode(text).decode('utf-8') 19 | 20 | def get_identity(token): 21 | return decode(token, options={"verify_signature":False, "verify_exp":False}).get('identity') 22 | 23 | def save_file(filename, text): 24 | try: 25 | f = open(WEB_UPLOADDIR + filename, 'w') 26 | f.write(text) 27 | f.close() 28 | except Exception as e: 29 | text = str(e) 30 | return text 31 | 32 | def is_level_easy(): 33 | mode = ServerMode.query.one() 34 | return mode.hardened == False 35 | 36 | def is_level_hard(): 37 | mode = ServerMode.query.one() 38 | return mode.hardened == True 39 | 40 | def set_mode(mode): 41 | mode = ServerMode.set_mode(mode) 42 | -------------------------------------------------------------------------------- /core/middleware.py: -------------------------------------------------------------------------------- 1 | import werkzeug 2 | 3 | from flask import request 4 | from core.decorators import run_only_once 5 | 6 | from core import ( 7 | helpers, 8 | parser, 9 | security 10 | ) 11 | 12 | # Middleware 13 | class DepthProtectionMiddleware(object): 14 | def resolve(self, next, root, info, **kwargs): 15 | if helpers.is_level_easy(): 16 | return next(root, info, **kwargs) 17 | 18 | depth = 0 19 | array_qry = [] 20 | 21 | if isinstance(info.context.json, dict): 22 | array_qry.append(info.context.json) 23 | 24 | elif isinstance(info.context.json, list): 25 | array_qry = info.context.json 26 | 27 | for q in array_qry: 28 | query = q.get('query', None) 29 | mutation = q.get('mutation', None) 30 | 31 | if query: 32 | depth = parser.get_depth(query) 33 | 34 | elif mutation: 35 | depth = parser.get_depth(query) 36 | 37 | if security.depth_exceeded(depth): 38 | raise werkzeug.exceptions.SecurityError('Query Depth Exceeded! Deep Recursion Attack Detected.') 39 | 40 | return next(root, info, **kwargs) 41 | 42 | class CostProtectionMiddleware(object): 43 | def resolve(self, next, root, info, **kwargs): 44 | if helpers.is_level_easy(): 45 | return next(root, info, **kwargs) 46 | 47 | fields_requested = [] 48 | array_qry = [] 49 | 50 | if isinstance(info.context.json, dict): 51 | array_qry.append(info.context.json) 52 | 53 | elif isinstance(info.context.json, list): 54 | array_qry = info.context.json 55 | 56 | for q in array_qry: 57 | query = q.get('query', None) 58 | mutation = q.get('mutation', None) 59 | 60 | if query: 61 | fields_requested += parser.get_fields_from_query(query) 62 | elif mutation: 63 | fields_requested += parser.get_fields_from_query(mutation) 64 | 65 | if security.cost_exceeded(fields_requested): 66 | raise werkzeug.exceptions.SecurityError('Cost of Query is too high.') 67 | 68 | return next(root, info, **kwargs) 69 | 70 | class OpNameProtectionMiddleware(object): 71 | @run_only_once 72 | def resolve(self, next, root, info, **kwargs): 73 | if helpers.is_level_easy(): 74 | return next(root, info, **kwargs) 75 | 76 | try: 77 | opname = info.operation.name.value 78 | except: 79 | opname = "No Operation" 80 | 81 | if opname != 'No Operation' and not security.operation_name_allowed(opname): 82 | raise werkzeug.exceptions.SecurityError('Operation Name "{}" is not allowed.'.format(opname)) 83 | 84 | return next(root, info, **kwargs) 85 | 86 | 87 | class processMiddleware(object): 88 | def resolve(self, next, root, info, **kwargs): 89 | if helpers.is_level_easy(): 90 | return next(root, info, **kwargs) 91 | 92 | array_qry = [] 93 | 94 | if info.context.json is not None: 95 | if isinstance(info.context.json, dict): 96 | array_qry.append(info.context.json) 97 | 98 | for q in array_qry: 99 | query = q.get('query', None) 100 | if security.on_denylist(query): 101 | raise werkzeug.exceptions.SecurityError('Query is on the Deny List.') 102 | 103 | return next(root, info, **kwargs) 104 | 105 | class IntrospectionMiddleware(object): 106 | @run_only_once 107 | def resolve(self, next, root, info, **kwargs): 108 | if helpers.is_level_easy(): 109 | return next(root, info, **kwargs) 110 | 111 | if info.field_name.lower() in ['__schema']: 112 | raise werkzeug.exceptions.SecurityError('Introspection is Disabled') 113 | 114 | return next(root, info, **kwargs) 115 | 116 | class IGQLProtectionMiddleware(object): 117 | @run_only_once 118 | def resolve(self, next, root, info, **kwargs): 119 | if helpers.is_level_hard(): 120 | raise werkzeug.exceptions.SecurityError('GraphiQL is disabled') 121 | 122 | cookie = request.cookies.get('env') 123 | if cookie and cookie == 'graphiql:enable': 124 | return next(root, info, **kwargs) 125 | 126 | raise werkzeug.exceptions.SecurityError('GraphiQL Access Rejected') 127 | -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from app import db 4 | import re 5 | from graphql import parse 6 | from graphql.execution.base import ResolveInfo 7 | 8 | # Models 9 | class User(db.Model): 10 | __tablename__ = 'users' 11 | id = db.Column(db.Integer, primary_key=True) 12 | username = db.Column(db.String(20),unique=True,nullable=False) 13 | email = db.Column(db.String(20),unique=True,nullable=False) 14 | password = db.Column(db.String(60),nullable=False) 15 | 16 | @classmethod 17 | def create_user(cls, **kw): 18 | obj = cls(**kw) 19 | db.session.add(obj) 20 | db.session.commit() 21 | 22 | return obj 23 | 24 | 25 | def clean_query(gql_query): 26 | clean = re.sub(r'(?<=token:")(.*)(?=")', "*****", gql_query) 27 | clean = re.sub(r'(?<=password:")(.*)(?=")', "*****", clean) 28 | return clean 29 | 30 | 31 | class Audit(db.Model): 32 | __tablename__ = 'audits' 33 | id = db.Column(db.Integer, primary_key=True) 34 | gqloperation = db.Column(db.String) 35 | gqlquery = db.Column(db.String) 36 | timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow) 37 | 38 | @classmethod 39 | def create_audit_entry(cls, info, subscription_type=False): 40 | gql_query = '{}' 41 | gql_operation = None 42 | obj = False 43 | 44 | """ 45 | GraphQL subscriptions pass a String, conversion to AST is required. 46 | """ 47 | if subscription_type and isinstance(info, str): 48 | """Subscriptions""" 49 | gql_query = info 50 | ast = parse(gql_query) 51 | 52 | try: 53 | gql_operation = ast.definitions[0].name.value 54 | except: 55 | gql_operation = 'No Operation' 56 | 57 | obj = cls(**{"gqloperation":gql_operation, "gqlquery":gql_query}) 58 | db.session.add(obj) 59 | else: 60 | """Queries and Mutations""" 61 | try: 62 | gql_operation = info.operation.name.value 63 | except: 64 | gql_operation = "No Operation" 65 | 66 | if isinstance(info, ResolveInfo): 67 | if isinstance(info.context.json, list): 68 | """Array-based Batch""" 69 | for i in info.context.json: 70 | gql_query = i.get("query") 71 | gql_query = clean_query(gql_query) 72 | obj = cls(**{"gqloperation":gql_operation, "gqlquery":gql_query}) 73 | db.session.add(obj) 74 | else: 75 | if info.context.json: 76 | gql_query = info.context.json.get("query") 77 | gql_query = clean_query(gql_query) 78 | obj = cls(**{"gqloperation":gql_operation, "gqlquery":gql_query}) 79 | db.session.add(obj) 80 | 81 | db.session.commit() 82 | return obj 83 | 84 | class Owner(db.Model): 85 | __tablename__ = 'owners' 86 | id = db.Column(db.Integer, primary_key=True) 87 | name = db.Column(db.String) 88 | paste = db.relationship('Paste', lazy='dynamic', overlaps="pastes") 89 | 90 | 91 | class Paste(db.Model): 92 | __tablename__ = 'pastes' 93 | id = db.Column(db.Integer, primary_key=True) 94 | title = db.Column(db.String) 95 | content = db.Column(db.String) 96 | public = db.Column(db.Boolean, default=False) 97 | user_agent = db.Column(db.String, default=None) 98 | ip_addr = db.Column(db.String) 99 | owner_id = db.Column(db.Integer, db.ForeignKey(Owner.id)) 100 | owner = db.relationship( 101 | Owner, 102 | backref='pastes', 103 | overlaps="paste" 104 | ) 105 | burn = db.Column(db.Boolean, default=False) 106 | 107 | @classmethod 108 | def create_paste(cls, **kw): 109 | obj = cls(**kw) 110 | db.session.add(obj) 111 | db.session.commit() 112 | 113 | return obj 114 | 115 | class ServerMode(db.Model): 116 | __tablename__ = 'servermode' 117 | id = db.Column(db.Integer, primary_key=True) 118 | hardened = db.Column(db.Boolean, default=False) 119 | 120 | @classmethod 121 | def set_mode(cls, mode): 122 | obj = ServerMode.query.one() 123 | if mode == 'easy': 124 | obj.hardened = False 125 | else: 126 | obj.hardened = True 127 | 128 | db.session.add(obj) 129 | db.session.commit() 130 | 131 | return obj -------------------------------------------------------------------------------- /core/parser.py: -------------------------------------------------------------------------------- 1 | def get_fields_from_query(q): 2 | fields = [k for k in q.split() if k.isalnum()] 3 | return fields 4 | 5 | def get_depth(q): 6 | depth = 0 7 | for i in q.split(): 8 | if i == '{': 9 | depth += 1 10 | return depth 11 | -------------------------------------------------------------------------------- /core/security.py: -------------------------------------------------------------------------------- 1 | import config 2 | import random 3 | import ipaddress 4 | import time 5 | 6 | from core import helpers 7 | 8 | def simulate_load(): 9 | loads = [200, 300, 400, 500] 10 | count = 0 11 | limit = random.choice(loads) 12 | while True: 13 | time.sleep(0.1) 14 | count += 1 15 | if count > limit: 16 | return 17 | 18 | def get_network(addr, style='cidr'): 19 | try: 20 | if style == 'cidr': 21 | return str(ipaddress.ip_network(addr)) 22 | else: 23 | return str(ipaddress.ip_network(addr).netmask) 24 | except: 25 | return 'Could not identify network' 26 | 27 | def is_port(port): 28 | if isinstance(port, int): 29 | if port >= 0 and port <= 65535: 30 | return True 31 | return False 32 | 33 | def allowed_cmds(cmd): 34 | if helpers.is_level_easy(): 35 | return True 36 | elif helpers.is_level_hard(): 37 | if cmd.startswith(('echo', 'ps' 'whoami', 'tail')): 38 | return True 39 | return False 40 | 41 | def strip_dangerous_characters(cmd): 42 | if helpers.is_level_easy(): 43 | return cmd 44 | elif helpers.is_level_hard(): 45 | return cmd.replace(';','').replace('&', '') 46 | return cmd 47 | 48 | def check_creds(username, password, real_password): 49 | if username != 'admin': 50 | return (False, 'Username is invalid') 51 | 52 | if password == real_password: 53 | return (True, 'Password Accepted.') 54 | 55 | return (False, 'Password Incorrect') 56 | 57 | def on_denylist(query): 58 | normalized_query = ''.join(query.split()) 59 | queries = [ 60 | 'query{systemHealth}', 61 | '{systemHealth}' 62 | ] 63 | 64 | if normalized_query in queries: 65 | return True 66 | return False 67 | 68 | def operation_name_allowed(operation_name): 69 | opnames_allowed = ['CreatePaste', 'CreateUser', 'EditPaste', 'getPastes', 'UploadPaste', 'ImportPaste'] 70 | if operation_name in opnames_allowed: 71 | return True 72 | return False 73 | 74 | def depth_exceeded(depth): 75 | depth_allowed = config.MAX_DEPTH 76 | if depth > depth_allowed: 77 | return True 78 | return False 79 | 80 | def cost_exceeded(qry_fields): 81 | total_cost_allowed = config.MAX_COST 82 | total_query_cost = 0 83 | 84 | field_cost = { 85 | 'systemUpdate':10, 86 | } 87 | 88 | for field in qry_fields: 89 | if field in field_cost: 90 | total_query_cost += field_cost[field] 91 | 92 | if total_query_cost > total_cost_allowed: 93 | return True 94 | 95 | return False -------------------------------------------------------------------------------- /core/view_override.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | import inspect 4 | 5 | from core.models import Audit 6 | from functools import partial 7 | from flask import Response, request 8 | from flask_graphql import GraphQLView 9 | from rx import AnonymousObservable 10 | from graphql_server import ( 11 | HttpQueryError, 12 | run_http_query, 13 | FormattedResult 14 | ) 15 | from graphql import GraphQLError 16 | from graphql.error import format_error as format_graphql_error 17 | from graphql_ws.gevent import GeventConnectionContext 18 | from graphql_ws.base_sync import BaseSyncSubscriptionServer 19 | from graphql_ws.base import ConnectionClosedException 20 | 21 | def format_custom_error(error): 22 | try: 23 | message = str(error) 24 | except UnicodeEncodeError: 25 | message = error.message.encode("utf-8") 26 | 27 | formatted_error = {"message": message} 28 | 29 | if isinstance(error, GraphQLError): 30 | if error.locations is not None: 31 | formatted_error["locations"] = [ 32 | {"line": loc.line, "column": loc.column} for loc in error.locations 33 | ] 34 | 35 | if error.path is not None: 36 | formatted_error["path"] = error.path 37 | 38 | if error.extensions is not None: 39 | formatted_error["extensions"] = error.extensions 40 | 41 | # A user who has not yet altered their cookie to bypass the GraphiQL protection will see the debug information and may get bamboozled 42 | # Do not show tracing error the first time a user hits the GraphiQL endpoint. 43 | if 'GraphiQL Access Rejected' not in message: 44 | if 'extensions' not in formatted_error: 45 | formatted_error['extensions'] = {} 46 | 47 | # Get some stack traces and caller file information 48 | frame = inspect.currentframe() 49 | caller_frame = inspect.stack()[0] 50 | caller_filename_full = caller_frame.filename 51 | 52 | formatted_error['extensions']['exception'] = {} 53 | formatted_error['extensions']['exception']['stack'] = traceback.format_stack(frame) 54 | formatted_error['extensions']['exception']['debug'] = traceback.format_exc() 55 | formatted_error['extensions']['exception']['path'] = caller_filename_full 56 | 57 | return formatted_error 58 | 59 | def format_execution_result(execution_result, format_error,): 60 | status_code = 200 61 | if execution_result: 62 | target_result = None 63 | 64 | def override_target_result(value): 65 | nonlocal target_result 66 | target_result = value 67 | 68 | if isinstance(execution_result, AnonymousObservable): 69 | target = execution_result.subscribe(on_next=lambda value: override_target_result(value)) 70 | target.dispose() 71 | response = target_result 72 | elif execution_result.invalid: 73 | status_code = 400 74 | response = execution_result.to_dict(format_error=format_error) 75 | else: 76 | response = execution_result.to_dict(format_error=format_error) 77 | else: 78 | response = None 79 | return FormattedResult(response, status_code) 80 | 81 | def encode_execution_results(execution_results, format_error, is_batch,encode): 82 | responses = [ 83 | format_execution_result(execution_result, format_error) 84 | for execution_result in execution_results 85 | ] 86 | result, status_codes = zip(*responses) 87 | status_code = max(status_codes) 88 | 89 | if not is_batch: 90 | result = result[0] 91 | 92 | return encode(result), status_code 93 | 94 | class OverriddenView(GraphQLView): 95 | def dispatch_request(self): 96 | try: 97 | request_method = request.method.lower() 98 | data = self.parse_body() 99 | 100 | show_graphiql = request_method == 'get' and self.should_display_graphiql() 101 | catch = show_graphiql 102 | pretty = self.pretty or show_graphiql or request.args.get('pretty') 103 | 104 | extra_options = {} 105 | executor = self.get_executor() 106 | if executor: 107 | # We only include it optionally since 108 | # executor is not a valid argument in all backends 109 | extra_options['executor'] = executor 110 | 111 | execution_results, all_params = run_http_query( 112 | self.schema, 113 | request_method, 114 | data, 115 | query_data=request.args, 116 | batch_enabled=self.batch, 117 | catch=catch, 118 | backend=self.get_backend(), 119 | 120 | # Execute options 121 | root=self.get_root_value(), 122 | context=self.get_context(), 123 | middleware=self.get_middleware(), 124 | **extra_options 125 | ) 126 | 127 | result, status_code = encode_execution_results( 128 | execution_results, 129 | is_batch=isinstance(data, list), 130 | format_error=self.format_error, 131 | encode=partial(self.encode, pretty=pretty) 132 | 133 | ) 134 | 135 | if show_graphiql: 136 | return self.render_graphiql( 137 | params=all_params[0], 138 | result=result 139 | ) 140 | 141 | return Response( 142 | result, 143 | status=status_code, 144 | content_type='application/json' 145 | ) 146 | 147 | except HttpQueryError as e: 148 | return Response( 149 | self.encode({ 150 | 'errors': [self.format_error(e)] 151 | }), 152 | status=e.status_code, 153 | headers=e.headers, 154 | content_type='application/json' 155 | ) 156 | 157 | class GeventSubscriptionServerCustom(BaseSyncSubscriptionServer): 158 | def handle(self, ws, request_context=None): 159 | connection_context = GeventConnectionContext(ws, request_context) 160 | self.on_open(connection_context) 161 | while True: 162 | try: 163 | if connection_context.closed: 164 | raise ConnectionClosedException() 165 | message = connection_context.receive() 166 | except ConnectionClosedException: 167 | self.on_close(connection_context) 168 | return 169 | 170 | if message: 171 | 172 | msg = json.loads(message) 173 | 174 | if msg.get('type', '') == 'start': 175 | Audit.create_audit_entry(msg['payload']['query'], subscription_type=True) 176 | 177 | 178 | self.on_message(connection_context, message) 179 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from graphql import GraphQLError 4 | 5 | from core import ( 6 | security, 7 | helpers, 8 | middleware 9 | ) 10 | 11 | from core.directives import * 12 | from core.models import ( 13 | Owner, 14 | Paste, 15 | User, 16 | Audit 17 | ) 18 | from core.view_override import ( 19 | OverriddenView, 20 | GeventSubscriptionServerCustom, 21 | format_custom_error 22 | ) 23 | from db.solutions import solutions as list_of_solutions 24 | from rx.subjects import Subject 25 | from flask import ( 26 | request, 27 | render_template, 28 | make_response 29 | ) 30 | 31 | from flask_graphql_auth import ( 32 | create_access_token, 33 | create_refresh_token, 34 | ) 35 | 36 | from flask_sockets import Sockets 37 | from graphql.backend import GraphQLCoreBackend 38 | from sqlalchemy import event, text 39 | from graphene_sqlalchemy import SQLAlchemyObjectType 40 | from core.helpers import get_identity 41 | from app import app, db 42 | 43 | from version import VERSION 44 | from config import WEB_HOST, WEB_PORT 45 | 46 | # SQLAlchemy Types 47 | class UserObject(SQLAlchemyObjectType): 48 | class Meta: 49 | model = User 50 | exclude_fields = ('email',) 51 | 52 | username = graphene.String(capitalize=graphene.Boolean()) 53 | 54 | @staticmethod 55 | def resolve_username(self, info, **kwargs): 56 | if kwargs.get('capitalize'): 57 | return self.username.capitalize() 58 | return self.username 59 | 60 | @staticmethod 61 | def resolve_password(self, info, **kwargs): 62 | if info.context.json.get('identity') == 'admin': 63 | return self.password 64 | else: 65 | return '******' 66 | 67 | class PasteObject(SQLAlchemyObjectType): 68 | class Meta: 69 | model = Paste 70 | 71 | def resolve_ip_addr(self, info): 72 | for field_ast in info.field_asts: 73 | for i in field_ast.directives: 74 | if i.name.value == 'show_network': 75 | if i.arguments[0].name.value == 'style': 76 | return security.get_network(self.ip_addr, style=i.arguments[0].value.value) 77 | return self.ip_addr 78 | 79 | class OwnerObject(SQLAlchemyObjectType): 80 | class Meta: 81 | model = Owner 82 | 83 | class AuditObject(SQLAlchemyObjectType): 84 | class Meta: 85 | model = Audit 86 | 87 | class UserInput(graphene.InputObjectType): 88 | username = graphene.String(required=True) 89 | email = graphene.String(required=True) 90 | password = graphene.String(required=True) 91 | 92 | class CreateUser(graphene.Mutation): 93 | class Arguments: 94 | user_data = UserInput(required=True) 95 | 96 | user = graphene.Field(lambda:UserObject) 97 | 98 | def mutate(root, info, user_data=None): 99 | user_obj = User.create_user( 100 | username=user_data.username, 101 | email=user_data.email, 102 | password=user_data.password 103 | ) 104 | 105 | Audit.create_audit_entry(info) 106 | 107 | return CreateUser(user=user_obj) 108 | 109 | class CreatePaste(graphene.Mutation): 110 | paste = graphene.Field(lambda:PasteObject) 111 | 112 | class Arguments: 113 | title = graphene.String() 114 | content = graphene.String() 115 | public = graphene.Boolean(required=False, default_value=True) 116 | burn = graphene.Boolean(required=False, default_value=False) 117 | 118 | def mutate(self, info, title, content, public, burn): 119 | owner = Owner.query.filter_by(name='DVGAUser').first() 120 | 121 | paste_obj = Paste.create_paste( 122 | title=title, 123 | content=content, public=public, burn=burn, 124 | owner_id=owner.id, owner=owner, ip_addr=request.remote_addr, 125 | user_agent=request.headers.get('User-Agent', '') 126 | ) 127 | 128 | Audit.create_audit_entry(info) 129 | 130 | return CreatePaste(paste=paste_obj) 131 | 132 | class EditPaste(graphene.Mutation): 133 | paste = graphene.Field(lambda:PasteObject) 134 | 135 | class Arguments: 136 | id = graphene.Int() 137 | title = graphene.String(required=False) 138 | content = graphene.String(required=False) 139 | 140 | def mutate(self, info, id, title=None, content=None): 141 | paste_obj = Paste.query.filter_by(id=id).first() 142 | 143 | if title == None: 144 | title = paste_obj.title 145 | if content == None: 146 | content = paste_obj.content 147 | 148 | Paste.query.filter_by(id=id).update(dict(title=title, content=content)) 149 | paste_obj = Paste.query.filter_by(id=id).first() 150 | 151 | db.session.commit() 152 | 153 | Audit.create_audit_entry(info) 154 | 155 | return EditPaste(paste=paste_obj) 156 | 157 | class DeletePaste(graphene.Mutation): 158 | result = graphene.Boolean() 159 | 160 | class Arguments: 161 | id = graphene.Int() 162 | 163 | 164 | def mutate(self, info, id): 165 | result = False 166 | 167 | if Paste.query.filter_by(id=id).delete(): 168 | result = True 169 | db.session.commit() 170 | 171 | Audit.create_audit_entry(info) 172 | 173 | return DeletePaste(result=result) 174 | 175 | class UploadPaste(graphene.Mutation): 176 | content = graphene.String() 177 | filename = graphene.String() 178 | 179 | class Arguments: 180 | content = graphene.String(required=True) 181 | filename = graphene.String(required=True) 182 | 183 | result = graphene.String() 184 | 185 | def mutate(self, info, filename, content): 186 | result = helpers.save_file(filename, content) 187 | owner = Owner.query.filter_by(name='DVGAUser').first() 188 | 189 | Paste.create_paste( 190 | title='Imported Paste from File - {}'.format(helpers.generate_uuid()), 191 | content=content, public=False, burn=False, 192 | owner_id=owner.id, owner=owner, ip_addr=request.remote_addr, 193 | user_agent=request.headers.get('User-Agent', '') 194 | ) 195 | 196 | Audit.create_audit_entry(info) 197 | 198 | return UploadPaste(result=result) 199 | 200 | class ImportPaste(graphene.Mutation): 201 | result = graphene.String() 202 | 203 | class Arguments: 204 | host = graphene.String(required=True) 205 | port = graphene.Int(required=False) 206 | path = graphene.String(required=True) 207 | scheme = graphene.String(required=True) 208 | 209 | def mutate(self, info, host='pastebin.com', port=443, path='/', scheme="http"): 210 | url = security.strip_dangerous_characters(f"{scheme}://{host}:{port}{path}") 211 | cmd = helpers.run_cmd(f'curl --insecure {url}') 212 | 213 | owner = Owner.query.filter_by(name='DVGAUser').first() 214 | Paste.create_paste( 215 | title='Imported Paste from URL - {}'.format(helpers.generate_uuid()), 216 | content=cmd, public=False, burn=False, 217 | owner_id=owner.id, owner=owner, ip_addr=request.remote_addr, 218 | user_agent=request.headers.get('User-Agent', '') 219 | ) 220 | 221 | Audit.create_audit_entry(info) 222 | 223 | return ImportPaste(result=cmd) 224 | 225 | class Login(graphene.Mutation): 226 | access_token = graphene.String() 227 | refresh_token = graphene.String() 228 | 229 | class Arguments: 230 | username = graphene.String() 231 | password = graphene.String() 232 | 233 | def mutate(self, info , username, password) : 234 | user = User.query.filter_by(username=username, password=password).first() 235 | Audit.create_audit_entry(info) 236 | if not user: 237 | raise Exception('Authentication Failure') 238 | return Login( 239 | access_token = create_access_token(username), 240 | refresh_token = create_refresh_token(username) 241 | ) 242 | 243 | class Mutations(graphene.ObjectType): 244 | create_paste = CreatePaste.Field() 245 | edit_paste = EditPaste.Field() 246 | delete_paste = DeletePaste.Field() 247 | upload_paste = UploadPaste.Field() 248 | import_paste = ImportPaste.Field() 249 | create_user = CreateUser.Field() 250 | login = Login.Field() 251 | 252 | global_event = Subject() 253 | 254 | @event.listens_for(Paste, 'after_insert') 255 | def new_paste(mapper,cconnection,target): 256 | global_event.on_next(target) 257 | 258 | class Subscription(graphene.ObjectType): 259 | paste = graphene.Field(PasteObject, id=graphene.Int(), title=graphene.String()) 260 | 261 | def resolve_paste(self, info): 262 | return global_event.map(lambda i: i) 263 | 264 | class SearchResult(graphene.Union): 265 | class Meta: 266 | types = (PasteObject, UserObject) 267 | 268 | 269 | class Query(graphene.ObjectType): 270 | pastes = graphene.List(PasteObject, public=graphene.Boolean(), limit=graphene.Int(), filter=graphene.String()) 271 | paste = graphene.Field(PasteObject, id=graphene.Int(), title=graphene.String()) 272 | system_update = graphene.String() 273 | system_diagnostics = graphene.String(username=graphene.String(), password=graphene.String(), cmd=graphene.String()) 274 | system_debug = graphene.String(arg=graphene.String()) 275 | system_health = graphene.String() 276 | users = graphene.List(UserObject, id=graphene.Int()) 277 | read_and_burn = graphene.Field(PasteObject, id=graphene.Int()) 278 | search = graphene.List(SearchResult, keyword=graphene.String()) 279 | audits = graphene.List(AuditObject) 280 | delete_all_pastes = graphene.Boolean() 281 | me = graphene.Field(UserObject, token=graphene.String()) 282 | 283 | def resolve_me(self, info, token): 284 | Audit.create_audit_entry(info) 285 | 286 | identity = get_identity(token) 287 | 288 | if info.context.json == None: 289 | raise GraphQLError("JSON payload was not found.") 290 | 291 | info.context.json['identity'] = identity 292 | 293 | query = UserObject.get_query(info) 294 | 295 | result = query.filter_by(username=identity).first() 296 | return result 297 | 298 | def resolve_search(self, info, keyword=None): 299 | Audit.create_audit_entry(info) 300 | items = [] 301 | if keyword: 302 | search = "%{}%".format(keyword) 303 | queryset1 = Paste.query.filter(Paste.title.like(search)) 304 | items.extend(queryset1) 305 | queryset2 = User.query.filter(User.username.like(search)) 306 | items.extend(queryset2) 307 | else: 308 | queryset1 = Paste.query.all() 309 | items.extend(queryset1) 310 | queryset2 = User.query.all() 311 | items.extend(queryset2) 312 | return items 313 | 314 | def resolve_pastes(self, info, public=False, limit=1000, filter=None): 315 | query = PasteObject.get_query(info) 316 | Audit.create_audit_entry(info) 317 | result = query.filter_by(public=public, burn=False) 318 | 319 | if filter: 320 | result = result.filter(text("title = '%s' or content = '%s'" % (filter, filter))) 321 | 322 | return result.order_by(Paste.id.desc()).limit(limit) 323 | 324 | def resolve_paste(self, info, id=None, title=None): 325 | query = PasteObject.get_query(info) 326 | Audit.create_audit_entry(info) 327 | if title: 328 | return query.filter_by(title=title, burn=False).first() 329 | 330 | return query.filter_by(id=id, burn=False).first() 331 | 332 | def resolve_system_update(self, info): 333 | security.simulate_load() 334 | Audit.create_audit_entry(info) 335 | return 'no updates available' 336 | 337 | def resolve_system_diagnostics(self, info, username, password, cmd='whoami'): 338 | q = User.query.filter_by(username='admin').first() 339 | real_passw = q.password 340 | res, msg = security.check_creds(username, password, real_passw) 341 | Audit.create_audit_entry(info) 342 | if res: 343 | output = f'{cmd}: command not found' 344 | if security.allowed_cmds(cmd): 345 | output = helpers.run_cmd(cmd) 346 | return output 347 | return msg 348 | 349 | def resolve_system_debug(self, info, arg=None): 350 | Audit.create_audit_entry(info) 351 | if arg: 352 | output = helpers.run_cmd('ps {}'.format(arg)) 353 | else: 354 | output = helpers.run_cmd('ps') 355 | return output 356 | 357 | def resolve_read_and_burn(self, info, id): 358 | result = Paste.query.filter_by(id=id, burn=True).first() 359 | Paste.query.filter_by(id=id, burn=True).delete() 360 | db.session.commit() 361 | Audit.create_audit_entry(info) 362 | return result 363 | 364 | def resolve_system_health(self, info): 365 | Audit.create_audit_entry(info) 366 | return 'System Load: {}'.format( 367 | helpers.run_cmd("uptime | awk -F': ' '{print $2}' | awk -F',' '{print $1}'") 368 | ) 369 | 370 | def resolve_users(self, info, id=None): 371 | query = UserObject.get_query(info) 372 | Audit.create_audit_entry(info) 373 | if id: 374 | result = query.filter_by(id=id) 375 | else: 376 | result = query 377 | 378 | return result 379 | 380 | def resolve_audits(self, info): 381 | query = Audit.query.all() 382 | Audit.create_audit_entry(info) 383 | return query 384 | 385 | def resolve_delete_all_pastes(self, info): 386 | Audit.create_audit_entry(info) 387 | Paste.query.delete() 388 | db.session.commit() 389 | return Paste.query.count() == 0 390 | 391 | 392 | @app.route('/') 393 | def index(): 394 | resp = make_response(render_template('index.html')) 395 | resp.set_cookie("env", "graphiql:disable") 396 | return resp 397 | 398 | @app.route('/about') 399 | def about(): 400 | return render_template("about.html") 401 | 402 | @app.route('/solutions') 403 | def solutions(): 404 | return render_template("solutions.html", solutions=list_of_solutions) 405 | 406 | @app.route('/create_paste') 407 | def create_paste(): 408 | return render_template("paste.html", page="create_paste") 409 | 410 | @app.route('/import_paste') 411 | def import_paste(): 412 | return render_template("paste.html", page="import_paste") 413 | 414 | @app.route('/upload_paste') 415 | def upload_paste(): 416 | return render_template("paste.html", page="upload_paste") 417 | 418 | @app.route('/my_pastes') 419 | def my_paste(): 420 | return render_template("paste.html", page="my_pastes") 421 | 422 | @app.route('/public_pastes') 423 | def public_paste(): 424 | return render_template("paste.html", page="public_pastes") 425 | 426 | @app.route('/audit') 427 | def audit(): 428 | audit = Audit.query.order_by(Audit.timestamp.desc()) 429 | return render_template("audit.html", audit=audit) 430 | 431 | @app.route('/start_over') 432 | def start_over(): 433 | msg = "Restored to default state." 434 | res = helpers.initialize() 435 | 436 | if 'done' not in res: 437 | msg="Could not restore to default state." 438 | 439 | return render_template('index.html', msg=msg) 440 | 441 | @app.route('/difficulty/') 442 | def difficulty(level): 443 | if level in ('easy', 'hard'): 444 | message = f'Changed difficulty level to {level.capitalize()}' 445 | else: 446 | message = 'Level must be Beginner or Expert.' 447 | level = 'easy' 448 | 449 | helpers.set_mode(level) 450 | 451 | return render_template('index.html', msg = message) 452 | 453 | 454 | @app.context_processor 455 | def get_difficulty(): 456 | level = None 457 | if helpers.is_level_easy(): 458 | level = 'easy' 459 | else: 460 | level = 'hard' 461 | return dict(difficulty=level) 462 | 463 | @app.context_processor 464 | def get_server_info(): 465 | return dict(version=VERSION, host=WEB_HOST, port=WEB_PORT) 466 | 467 | 468 | @app.before_request 469 | def set_difficulty(): 470 | mode_header = request.headers.get('X-DVGA-MODE', None) 471 | if mode_header: 472 | if mode_header == 'Expert': 473 | helpers.set_mode('hard') 474 | else: 475 | helpers.set_mode('easy') 476 | 477 | schema = graphene.Schema(query=Query, mutation=Mutations, subscription=Subscription, directives=[ShowNetworkDirective, SkipDirective, DeprecatedDirective]) 478 | 479 | subscription_server = GeventSubscriptionServerCustom(schema) 480 | 481 | sockets = Sockets(app) 482 | 483 | @sockets.route('/subscriptions') 484 | def echo_socket(ws): 485 | 486 | subscription_server.handle(ws) 487 | 488 | return [] 489 | 490 | 491 | gql_middlew = [ 492 | middleware.CostProtectionMiddleware(), 493 | middleware.DepthProtectionMiddleware(), 494 | middleware.IntrospectionMiddleware(), 495 | middleware.processMiddleware(), 496 | middleware.OpNameProtectionMiddleware() 497 | ] 498 | 499 | igql_middlew = [ 500 | middleware.IGQLProtectionMiddleware() 501 | ] 502 | 503 | class CustomBackend(GraphQLCoreBackend): 504 | def __init__(self, executor=None): 505 | super().__init__(executor) 506 | self.execute_params['allow_subscriptions'] = True 507 | 508 | app.add_url_rule('/graphql', view_func=OverriddenView.as_view( 509 | 'graphql', 510 | schema=schema, 511 | middleware=gql_middlew, 512 | backend=CustomBackend(), 513 | batch=True 514 | )) 515 | 516 | app.add_url_rule('/graphiql', view_func=OverriddenView.as_view( 517 | 'graphiql', 518 | schema = schema, 519 | backend=CustomBackend(), 520 | graphiql = True, 521 | middleware = igql_middlew, 522 | format_error=format_custom_error 523 | )) 524 | 525 | 526 | -------------------------------------------------------------------------------- /db/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /db/agents.py: -------------------------------------------------------------------------------- 1 | agents = [ 2 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41', 3 | 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)', 4 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0', 5 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11.1; rv:85.0) Gecko/20100101 Firefox/85.0', 6 | 'Mozilla/5.0 (X11; Linux i686; rv:85.0) Gecko/20100101 Firefox/85.0', 7 | 'Mozilla/5.0 (Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0' 8 | ] -------------------------------------------------------------------------------- /db/content.py: -------------------------------------------------------------------------------- 1 | content = [ 2 | 'Sixty-Four comes asking for bread', 3 | 'He is good at eating pickles and telling women about his emotional problems.', 4 | 'He used to get confused between soldiers and shoulders, but as a military man, he now soldiers responsibility.', 5 | 'Pantyhose and heels are an interesting choice of attire for the beach.', 6 | 'I checked to make sure that he was still alive.', 7 | 'There is an art to getting your way, and spitting olive pits across the table is not it.', 8 | 'One advanced diverted', 9 | 'Too end instrument possession contrasted motionless', 10 | 'Mind your own business!', 11 | 'You are the Weakest Link! Goodbye!', 12 | 'Will you pick me up at my place?', 13 | 'What a big pumpkin!', 14 | 'quick trip back to Tennessee for the weekend!', 15 | 'I was excited to spend time with my wife without being interrupted by kids.', 16 | 'What are some things you don’t like to do and why?', 17 | 'These are used for cautioning someone to wait and not make a bad decision or take reckless action.', 18 | 'I am always late and I do not care.', 19 | 'They live in a beautiful house.', 20 | 'Do you know that man with a big hat on?', 21 | 'Where do you live?', 22 | 'Why can you open the door?', 23 | 'He reminds me of your brother.', 24 | 'How big you are!', 25 | 'What does your room look like?', 26 | 'Will you be free tomorrow morning?', 27 | 'The sun rises at the east.', 28 | 'How big is your house?' 29 | ] 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /db/owners.py: -------------------------------------------------------------------------------- 1 | owners = [ 2 | "Darlleen", 3 | "Robina", 4 | "Annamaria", 5 | "Erinn", 6 | "Kathy", 7 | "Nance", 8 | "Lavena", 9 | "Cinda", 10 | "Alvina", 11 | "Joni", 12 | "Karleen", 13 | "Heath", 14 | "Aggy", 15 | "Dominga", 16 | "Jena", 17 | "Grayce", 18 | "Hedvig", 19 | "Joellen", 20 | "Lela", 21 | "Stace", 22 | "Lula", 23 | "Latrina", 24 | "Tedda", 25 | "Christy", 26 | "Lora", 27 | "Ginnie", 28 | "Allx", 29 | "Jeanette", 30 | "Georgeta", 31 | "Charissa", 32 | "Kaile", 33 | "Maryl", 34 | "Karlene", 35 | "Bonni", 36 | "Gennifer", 37 | "Ardelle", 38 | "Chelsae", 39 | "Anstice", 40 | "Eulalie", 41 | "Darcee", 42 | "Beverley", 43 | "Emelyne", 44 | "Johnath", 45 | "Linnie", 46 | "Charo", 47 | "Carleen", 48 | "Sarette", 49 | "Hedvige", 50 | "Robena", 51 | "Flo" 52 | ] -------------------------------------------------------------------------------- /db/solutions.py: -------------------------------------------------------------------------------- 1 | solutions = [ 2 | "partials/solutions/solution_1.html", 3 | "partials/solutions/solution_2.html", 4 | "partials/solutions/solution_3.html", 5 | "partials/solutions/solution_4.html", 6 | "partials/solutions/solution_5.html", 7 | "partials/solutions/solution_6.html", 8 | "partials/solutions/solution_7.html", 9 | "partials/solutions/solution_8.html", 10 | "partials/solutions/solution_9.html", 11 | "partials/solutions/solution_10.html", 12 | "partials/solutions/solution_11.html", 13 | "partials/solutions/solution_12.html", 14 | "partials/solutions/solution_13.html", 15 | "partials/solutions/solution_14.html", 16 | "partials/solutions/solution_15.html", 17 | "partials/solutions/solution_16.html", 18 | "partials/solutions/solution_17.html", 19 | "partials/solutions/solution_18.html", 20 | "partials/solutions/solution_19.html", 21 | "partials/solutions/solution_20.html", 22 | "partials/solutions/solution_21.html", 23 | "partials/solutions/solution_22.html", 24 | ] -------------------------------------------------------------------------------- /db/titles.py: -------------------------------------------------------------------------------- 1 | titles = [ 2 | 'This is my first paste', 3 | 'Testing Testing', 4 | 'Whoa this is cool', 5 | 'TITLE!!!!!', 6 | 'What is this even' 7 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==7.0.0 2 | attrs==22.1.0 3 | bidict==0.22.0 4 | certifi==2022.12.7 5 | charset-normalizer==2.1.1 6 | click==8.1.3 7 | exceptiongroup==1.0.4 8 | Flask==2.2.2 9 | Flask-GraphQL==2.0.1 10 | Flask-GraphQL-Auth==1.3.3 11 | Flask-SocketIO==5.3.2 12 | Flask-SQLAlchemy==3.0.2 13 | gevent==22.10.2 14 | gevent-websocket==0.10.1 15 | graphene==2.1.9 16 | graphene-sqlalchemy==2.3.0 17 | graphql-core==2.3.2 18 | graphql-relay==2.0.1 19 | graphql-server-core==1.2.0 20 | graphql-ws==0.4.4 21 | greenlet==2.0.1 22 | h11==0.14.0 23 | idna==3.4 24 | iniconfig==1.1.1 25 | itsdangerous==2.1.2 26 | Jinja2==3.1.2 27 | MarkupSafe==2.1.1 28 | packaging==22.0 29 | pluggy==1.0.0 30 | promise==2.3 31 | PyJWT==2.0.1 32 | pytest==7.2.0 33 | python-engineio==4.3.4 34 | python-socketio==5.7.2 35 | requests==2.28.1 36 | Rx==1.6.1 37 | simple-websocket==0.9.0 38 | singledispatch==3.7.0 39 | six==1.16.0 40 | SQLAlchemy==1.4.44 41 | tomli==2.0.1 42 | urllib3==1.26.13 43 | Werkzeug==2.2.2 44 | wsproto==1.2.0 45 | zope.event==4.5.0 46 | zope.interface==5.5.2 47 | git+https://github.com/dolevf/flask-sockets@master -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import shutil 4 | import config 5 | import random 6 | 7 | from ipaddress import IPv4Network 8 | 9 | from app import db, app 10 | 11 | from core.models import Paste, Owner, User, ServerMode 12 | 13 | from db.agents import agents 14 | from db.owners import owners 15 | from db.titles import titles 16 | from db.content import content 17 | 18 | def clean_up(): 19 | if os.path.exists(config.WEB_UPLOADDIR): 20 | shutil.rmtree(config.WEB_UPLOADDIR) 21 | os.mkdir(config.WEB_UPLOADDIR) 22 | else: 23 | os.mkdir(config.WEB_UPLOADDIR) 24 | 25 | print('Reconstructing Database') 26 | if os.path.exists(config.SQLALCHEMY_FILE): 27 | try: 28 | os.remove(config.SQLALCHEMY_FILE) 29 | except OSError: 30 | pass 31 | return 32 | 33 | def random_title(): 34 | return random.choice(titles) 35 | 36 | def random_content(): 37 | return random.choice(content) 38 | 39 | def random_owner(): 40 | return random.choice(owners) 41 | 42 | def random_address(): 43 | addresses = [] 44 | for addr in IPv4Network('215.0.2.0/24'): 45 | addresses.append(str(addr)) 46 | return random.choice(addresses) 47 | 48 | def random_password(): 49 | weak_passwords = ['changeme'] 50 | return random.choice(weak_passwords) 51 | 52 | def random_useragent(): 53 | user_agents = [] 54 | for uas in agents: 55 | user_agents.append(uas) 56 | return random.choice(user_agents) 57 | 58 | def pump_db(): 59 | print('Populating Database') 60 | with app.app_context(): 61 | db.create_all() 62 | 63 | admin = User(username="admin", email="admin@blackhatgraphql.com", password=random_password()) 64 | operator = User(username="operator", email="operator@blackhatgraphql.com", password="password123") 65 | # create tokens for admin & operator 66 | 67 | db.session.add(admin) 68 | db.session.add(operator) 69 | 70 | owner = Owner(name='DVGAUser') 71 | db.session.add(owner) 72 | 73 | paste = Paste() 74 | paste.title = 'Testing Testing' 75 | paste.content = "My First Paste" 76 | paste.public = False 77 | paste.owner_id = owner.id 78 | paste.owner = owner 79 | paste.ip_addr = '127.0.0.1' 80 | paste.user_agent = 'User-Agent not set' 81 | db.session.add(paste) 82 | 83 | paste = Paste() 84 | paste.title = '555-555-1337' 85 | paste.content = "My Phone Number" 86 | paste.public = False 87 | paste.owner_id = owner.id 88 | paste.owner = owner 89 | paste.ip_addr = '127.0.0.1' 90 | paste.user_agent = 'User-Agent not set' 91 | db.session.add(paste) 92 | 93 | db.session.commit() 94 | 95 | for _ in range(0, 10): 96 | owner = Owner(name=random_owner()) 97 | paste = Paste() 98 | paste.title = random_title() 99 | paste.content = random_content() 100 | paste.public = True 101 | paste.owner_id = owner.id 102 | paste.owner = owner 103 | paste.ip_addr = random_address() 104 | paste.user_agent = random_useragent() 105 | 106 | db.session.add(owner) 107 | db.session.add(paste) 108 | 109 | mode = ServerMode() 110 | mode.hardened = False 111 | db.session.add(mode) 112 | 113 | db.session.commit() 114 | 115 | print('done') 116 | 117 | if __name__ == '__main__': 118 | clean_up() 119 | pump_db() 120 | sys.exit() 121 | -------------------------------------------------------------------------------- /static/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([class]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([class]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | -ms-overflow-style: scrollbar; 159 | } 160 | 161 | figure { 162 | margin: 0 0 1rem; 163 | } 164 | 165 | img { 166 | vertical-align: middle; 167 | border-style: none; 168 | } 169 | 170 | svg { 171 | overflow: hidden; 172 | vertical-align: middle; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | } 178 | 179 | caption { 180 | padding-top: 0.75rem; 181 | padding-bottom: 0.75rem; 182 | color: #6c757d; 183 | text-align: left; 184 | caption-side: bottom; 185 | } 186 | 187 | th { 188 | text-align: inherit; 189 | text-align: -webkit-match-parent; 190 | } 191 | 192 | label { 193 | display: inline-block; 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | button { 198 | border-radius: 0; 199 | } 200 | 201 | button:focus { 202 | outline: 1px dotted; 203 | outline: 5px auto -webkit-focus-ring-color; 204 | } 205 | 206 | input, 207 | button, 208 | select, 209 | optgroup, 210 | textarea { 211 | margin: 0; 212 | font-family: inherit; 213 | font-size: inherit; 214 | line-height: inherit; 215 | } 216 | 217 | button, 218 | input { 219 | overflow: visible; 220 | } 221 | 222 | button, 223 | select { 224 | text-transform: none; 225 | } 226 | 227 | [role="button"] { 228 | cursor: pointer; 229 | } 230 | 231 | select { 232 | word-wrap: normal; 233 | } 234 | 235 | button, 236 | [type="button"], 237 | [type="reset"], 238 | [type="submit"] { 239 | -webkit-appearance: button; 240 | } 241 | 242 | button:not(:disabled), 243 | [type="button"]:not(:disabled), 244 | [type="reset"]:not(:disabled), 245 | [type="submit"]:not(:disabled) { 246 | cursor: pointer; 247 | } 248 | 249 | button::-moz-focus-inner, 250 | [type="button"]::-moz-focus-inner, 251 | [type="reset"]::-moz-focus-inner, 252 | [type="submit"]::-moz-focus-inner { 253 | padding: 0; 254 | border-style: none; 255 | } 256 | 257 | input[type="radio"], 258 | input[type="checkbox"] { 259 | box-sizing: border-box; 260 | padding: 0; 261 | } 262 | 263 | textarea { 264 | overflow: auto; 265 | resize: vertical; 266 | } 267 | 268 | fieldset { 269 | min-width: 0; 270 | padding: 0; 271 | margin: 0; 272 | border: 0; 273 | } 274 | 275 | legend { 276 | display: block; 277 | width: 100%; 278 | max-width: 100%; 279 | padding: 0; 280 | margin-bottom: .5rem; 281 | font-size: 1.5rem; 282 | line-height: inherit; 283 | color: inherit; 284 | white-space: normal; 285 | } 286 | 287 | progress { 288 | vertical-align: baseline; 289 | } 290 | 291 | [type="number"]::-webkit-inner-spin-button, 292 | [type="number"]::-webkit-outer-spin-button { 293 | height: auto; 294 | } 295 | 296 | [type="search"] { 297 | outline-offset: -2px; 298 | -webkit-appearance: none; 299 | } 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | ::-webkit-file-upload-button { 306 | font: inherit; 307 | -webkit-appearance: button; 308 | } 309 | 310 | output { 311 | display: inline-block; 312 | } 313 | 314 | summary { 315 | display: list-item; 316 | cursor: pointer; 317 | } 318 | 319 | template { 320 | display: none; 321 | } 322 | 323 | [hidden] { 324 | display: none !important; 325 | } 326 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /static/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /static/extra.css: -------------------------------------------------------------------------------- 1 | .withlove { 2 | font-size:12px; 3 | font-weight: bold; 4 | color:purple; 5 | letter-spacing: 2px; 6 | text-decoration: none; 7 | } 8 | 9 | #toc_container { 10 | background: #f9f9f9 none repeat scroll 0 0; 11 | border: 1px solid #aaa; 12 | display: table; 13 | font-size: 95%; 14 | margin-bottom: 1em; 15 | padding: 20px; 16 | width: auto; 17 | } 18 | 19 | .toc_title { 20 | font-weight: 700; 21 | text-align: center; 22 | } 23 | 24 | #toc_container li, #toc_container ul, #toc_container ul li{ 25 | list-style: outside none none !important; 26 | } 27 | 28 | .reveal { 29 | width:40px; 30 | font-size:8px; 31 | color:white; 32 | background-color:green; 33 | font-weight:bold; 34 | } 35 | 36 | pre.bash { 37 | background-color: #000; 38 | border: 1px solid #000; 39 | color: white; 40 | padding: 8px; 41 | font-family: courier new; 42 | } 43 | 44 | div.pastebox { 45 | background-color: lightgrey; 46 | width: 300px; 47 | border: 3px solid purple; 48 | padding: 50px; 49 | margin: 20px; 50 | word-wrap: break-word; 51 | } 52 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/favicon.ico -------------------------------------------------------------------------------- /static/images/dvgql_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/images/dvgql_logo.png -------------------------------------------------------------------------------- /static/jquery/graphql.js: -------------------------------------------------------------------------------- 1 | (function(){function __extend(){var extended={},deep=false,i=0,length=arguments.length;if(Object.prototype.toString.call(arguments[0])=="[object Boolean]"){deep=arguments[0];i++}var merge=function(obj){for(var prop in obj){if(Object.prototype.hasOwnProperty.call(obj,prop)){if(deep&&Object.prototype.toString.call(obj[prop])=="[object Object]"){extended[prop]=__extend(true,extended[prop],obj[prop])}else{extended[prop]=obj[prop]}}}};for(;i0&&fragmentRegexp.test(fragment)){that.collectFragments(fragment,fragments).forEach(function(fragment){collectedFragments.unshift(fragment)})}}});return __unique(collectedFragments)};GraphQLClient.prototype.processQuery=function(query,fragments){if(typeof query=="object"&&query.hasOwnProperty("kind")&&query.hasOwnProperty("definitions")){throw new Error("Do not use graphql AST to send requests. Please generate query as string first using `graphql.print(query)`")}var fragmentRegexp=GraphQLClient.FRAGMENT_PATTERN;var collectedFragments=this.collectFragments(query,fragments);query=query.replace(fragmentRegexp,function(_,$m){return"... "+$m.split(".").join(FRAGMENT_SEPERATOR)});return[query].concat(collectedFragments.filter(function(fragment){return!query.match(fragment)})).join("\n")};GraphQLClient.prototype.autoDeclare=function(query,variables){var that=this;var typeMap={string:"String",number:function(value){return value%1===0?"Int":"Float"},boolean:"Boolean"};return query.replace(GraphQLClient.AUTODECLARE_PATTERN,function(){var types=[];for(var key in variables){var value=variables[key];var keyAndType=key.split(/^(.*?)\!/);if(keyAndType.length>1){keyAndType=keyAndType.slice(1);keyAndType[1]=keyAndType[1].replace(/(.*?)\!$/,"$1")}var mapping=typeMap[typeof value];var mappedType=typeof mapping==="function"?mapping(value):mapping;if(!key.match("!")&&keyAndType[0].match(/_?id/i)){mappedType="ID"}var type=keyAndType[1]||mappedType;if(type){types.push("$"+keyAndType[0]+": "+type+"!")}}types=types.join(", ");return types?"("+types+")":""})};GraphQLClient.prototype.cleanAutoDeclareAnnotations=function(variables){if(!variables)variables={};var newVariables={};for(var key in variables){var value=variables[key];var keyAndType=key.split("!");newVariables[keyAndType[0]]=value}return newVariables};GraphQLClient.prototype.buildFragments=function(fragments){var that=this;fragments=this.flatten(fragments||{});var fragmentObject={};for(var name in fragments){var fragment=fragments[name];if(typeof fragment=="object"){fragmentObject[name]=that.buildFragments(fragment)}else{fragmentObject[name]="\nfragment "+name+" "+fragment}}return fragmentObject};GraphQLClient.prototype.buildQuery=function(query,variables){return this.autoDeclare(this.processQuery(query,this._fragments),variables)};GraphQLClient.prototype.parseType=function(query){var match=query.trim().match(/^(query|mutation|subscription)/);if(!match)return"query";return match[1]};GraphQLClient.prototype.createSenderFunction=function(debug){var that=this;return function(query,originalQuery,type){if(__isTagCall(query)){return that.run(that.ql.apply(that,arguments))}var caller=function(variables,requestOptions){if(!requestOptions)requestOptions={};if(!variables)variables={};var fragmentedQuery=that.buildQuery(query,variables);var headers=__extend(that.options.headers||{},requestOptions.headers||{});return new Promise(function(resolve,reject){__request(debug,that.options.method||"post",that.getUrl(),headers,{query:fragmentedQuery,variables:that.cleanAutoDeclareAnnotations(variables)},!!that.options.asJSON,that.options.onRequestError,function(response,status){if(status==200){if(response.errors){reject(response.errors)}else if(response.data){resolve(response.data)}else{resolve(response)}}else{reject(response)}})})};caller.merge=function(mergeName,variables){if(!type){type=that.parseType(query);query=query.trim().replace(/^(query|mutation|subscription)\s*/,"").trim().replace(GraphQLClient.AUTODECLARE_PATTERN,"").trim().replace(/^\{|\}$/g,"")}if(!originalQuery){originalQuery=query}that._transaction[mergeName]=that._transaction[mergeName]||{query:[],mutation:[]};return new Promise(function(resolve){that._transaction[mergeName][type].push({type:type,query:originalQuery,variables:variables,resolver:resolve})})};if(arguments.length>3){return caller.apply(null,Array.prototype.slice.call(arguments,3))}return caller}};GraphQLClient.prototype.commit=function(mergeName){if(!this._transaction[mergeName]){throw new Error("You cannot commit the merge "+mergeName+" without creating it first.")}var that=this;var resolveMap={};var mergedVariables={};var mergedQueries={};Object.keys(this._transaction[mergeName]).forEach(function(method){if(that._transaction[mergeName][method].length===0)return;var subQuery=that._transaction[mergeName][method].map(function(merge){var reqId="merge"+Math.random().toString().split(".")[1].substr(0,6);resolveMap[reqId]=merge.resolver;var query=merge.query.replace(/\$([^\.\,\s\)]*)/g,function(_,m){if(!merge.variables){throw new Error("Unused variable on merge "+mergeName+": $"+m[0])}var matchingKey=Object.keys(merge.variables).filter(function(key){return key===m||key.match(new RegExp("^"+m+"!"))})[0];var variable=reqId+"__"+matchingKey;mergedVariables[method]=mergedVariables[method]||{};mergedVariables[method][variable]=merge.variables[matchingKey];return"$"+variable.split("!")[0]});query=query.replace(/^\{|\}\s*$/g,"").trim();var alias=query.trim().match(/^[^\(]+\:/);if(!alias){alias=query.match(/^[^\(\{]+/)[0]+":"}else{query=query.replace(/^[^\(]+\:/,"")}return reqId+"_"+alias+query}).join("\n");mergedQueries[method]=mergedQueries[method]||[];mergedQueries[method].push(method+" (@autodeclare) {\n"+subQuery+"\n }")});return Promise.all(Object.keys(mergedQueries).map(function(method){var query=mergedQueries[method].join("\n");var variables=mergedVariables[method];return that._sender(query,query,null,variables)})).then(function(responses){var newResponses={};responses.forEach(function(response){Object.keys(response).forEach(function(mergeKey){var parsedKey=mergeKey.match(/^(merge\d+)\_(.*)/);if(!parsedKey){throw new Error("Multiple root keys detected on response. Merging doesn't support it yet.")}var reqId=parsedKey[1];var fieldName=parsedKey[2];var newResponse={};newResponse[fieldName]=response[mergeKey];newResponses[fieldName]=(newResponses[fieldName]||[]).concat([response[mergeKey]]);resolveMap[reqId](newResponse)})});return newResponses}).catch(function(responses){return{error:true,errors:responses}}).finally(function(responses){that._transaction[mergeName]={query:[],mutation:[]};return responses})};GraphQLClient.prototype.createHelpers=function(sender){var that=this;function helper(query){if(__isTagCall(query)){that.__prefix=this.prefix;that.__suffix=this.suffix;var result=that.run(that.ql.apply(that,arguments));that.__prefix="";that.__suffix="";return result}var caller=sender(this.prefix+" "+query+" "+this.suffix,query.trim(),this.type);if(arguments.length>1&&arguments[1]!=null){return caller.apply(null,Array.prototype.slice.call(arguments,1))}return caller}var helpers=[{method:"mutate",type:"mutation"},{method:"query",type:"query"},{method:"subscribe",type:"subscription"}];helpers.forEach(function(m){that[m.method]=function(query,variables,options){if(that.options.alwaysAutodeclare===true||options&&options.declare===true){return helper.call({type:m.type,prefix:m.type+" (@autodeclare) {",suffix:"}"},query,variables)}return helper.call({type:m.type,prefix:m.type,suffix:""},query,variables)};that[m.method].run=function(query,options){return that[m.method](query,options)({})}});this.run=function(query){return sender(query,originalQuery,m.type,{})}};GraphQLClient.prototype.fragments=function(){return this._fragments};GraphQLClient.prototype.getOptions=function(){return this.options||{}};GraphQLClient.prototype.headers=function(newHeaders){return this.options.headers=__extend(this.options.headers,newHeaders)};GraphQLClient.prototype.fragment=function(fragment){if(typeof fragment=="string"){var _fragment=this._fragments[fragment.replace(/\./g,FRAGMENT_SEPERATOR)];if(!_fragment){throw"Fragment "+fragment+" not found!"}return _fragment.trim()}else{this.options.fragments=__extend(true,this.options.fragments,fragment);this._fragments=this.buildFragments(this.options.fragments);return this._fragments}};GraphQLClient.prototype.ql=function(strings){fragments=Array.prototype.slice.call(arguments,1);fragments=fragments.map(function(fragment){if(typeof fragment=="string"){return fragment.match(/fragment\s+([^\s]*)\s/)[1]}});var query=typeof strings=="string"?strings:strings.reduce(function(acc,seg,i){return acc+fragments[i-1]+seg});query=this.buildQuery(query);query=((this.__prefix||"")+" "+query+" "+(this.__suffix||"")).trim();return query};(function(root,factory){if(typeof define==="function"&&define.amd){define(function(){return root.graphql=factory(GraphQLClient)})}else if(typeof module==="object"&&module.exports){module.exports=factory(root.GraphQLClient)}else{root.graphql=factory(root.GraphQLClient)}})(this||self,function(){return GraphQLClient})})(); -------------------------------------------------------------------------------- /static/screenshots/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/screenshots/create.png -------------------------------------------------------------------------------- /static/screenshots/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/screenshots/index.png -------------------------------------------------------------------------------- /static/screenshots/pastes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/screenshots/pastes.png -------------------------------------------------------------------------------- /static/screenshots/solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/screenshots/solution.png -------------------------------------------------------------------------------- /static/simple-sidebar.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Simple Sidebar (https://startbootstrap.com/template/simple-sidebar) 3 | * Copyright 2013-2020 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-simple-sidebar/blob/master/LICENSE) 5 | */ 6 | 7 | #wrapper { 8 | overflow-x: hidden; 9 | } 10 | 11 | #sidebar-wrapper { 12 | min-height: 100vh; 13 | margin-left: -15rem; 14 | -webkit-transition: margin .25s ease-out; 15 | -moz-transition: margin .25s ease-out; 16 | -o-transition: margin .25s ease-out; 17 | transition: margin .25s ease-out; 18 | } 19 | 20 | /* #sidebar-wrapper .sidebar-heading { 21 | padding: 0.875rem 1.25rem; 22 | font-size: 1.2rem; 23 | } */ 24 | 25 | #sidebar-wrapper .list-group { 26 | width: 15rem; 27 | } 28 | 29 | #page-content-wrapper { 30 | min-width: 100vw; 31 | } 32 | 33 | #wrapper.toggled #sidebar-wrapper { 34 | margin-left: 0; 35 | } 36 | 37 | @media (min-width: 768px) { 38 | #sidebar-wrapper { 39 | margin-left: 0; 40 | } 41 | 42 | #page-content-wrapper { 43 | min-width: 0; 44 | width: 100%; 45 | } 46 | 47 | #wrapper.toggled #sidebar-wrapper { 48 | margin-left: -15rem; 49 | } 50 | } 51 | 52 | .c-red { 53 | color:red 54 | } 55 | 56 | .c-green { 57 | color:green 58 | } 59 | 60 | 61 | .c-admin { 62 | color:green 63 | } 64 | 65 | .c-user { 66 | color:red 67 | } 68 | 69 | .c-guest { 70 | color:red 71 | } -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolevf/Damn-Vulnerable-GraphQL-Application/a961308c02d1fb462b192681c336b0739e432da7/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/base.html' %} 2 | 3 | {% block header %} 4 | {% include 'partials/base_header.html' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | {% include 'partials/navbar.html' %} 11 |
12 |

About

13 |
14 |
15 |
16 |

What is GraphQL?

17 |

GraphQL is an open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data, made by Facebook.

18 |
19 |

What is Damn Vulnerable GraphQL Application?

20 |

Damn Vulnerable GraphQL is a weak implementation of GraphQL that provides a safe environment to understand GraphQL as a technology and its attack surface.

21 |

DVGA has numerous flaws, such as Injections, Code execution, Broken Access Controls, and more. For the full list of vulnerabilities, see Solutions page

22 |
23 |

Current Version

24 |

{{version}}

25 |
26 |
27 |
28 |
29 | 30 | {% endblock %} 31 | 32 | {% block scripts %} 33 | {% include 'partials/base_scripts.html' %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /templates/audit.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/base.html' %} 2 | 3 | {% block header %} 4 | {% include 'partials/base_header.html' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | {% include 'partials/navbar.html' %} 11 |
12 |

Audit

13 |
14 |
15 |
16 |

Recent Audit Log Activity

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for i in audit %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% endfor %} 38 |
#NameGraphQL OperationGraphQL QueryDate
{{loop.index}}. DVGAUser{{i.gqloperation}}{{i.gqlquery}}{{i.timestamp.strftime('%Y-%m-%d %H:%M:%S')}}
39 |
40 |
41 |
42 |
43 | 44 | {% endblock %} 45 | 46 | {% block scripts %} 47 | {% include 'partials/base_scripts.html' %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/base.html' %} 2 | 3 | {% block header %} 4 | {% include 'partials/base_header.html' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | {% include 'partials/navbar.html' %} 11 |
12 | {% if msg %} 13 |
14 |

System Message

15 |

{{msg}}

16 |
17 | {% endif %} 18 |

Damn Vulnerable GraphQL Application

19 |
20 |

Welcome!

21 |

22 | Damn Vulnerable GraphQL Application, or DVGA, is a vulnerable GraphQL implementation. DVGA allows learning how GraphQL can be exploited as well as defended in a safe environment. 23 |

24 |
25 |

Getting Started

26 |

27 | If you aren't yet familiar with GraphQL, see the GraphQL Resources section below. Otherwise, start poking around and find loopholes! There are GraphQL 28 | Implementation flaws as well as general application vulnerabilities. 29 |

30 |

You can set a "game mode" in DVGA: A beginner level or expert level by clicking on the top bar menu's cube icon and choosing the level. This is a global setting that will apply to all clients (GUI or CLI)

31 |

If you are interacting with DVGA programmatically, you can also set the game mode by passing the HTTP Request Header X-DVGA-MODE set to either Beginner or Expert as values.

32 |

If the Header is not set, DVGA will default to Beginner mode or to whatever you previously set in the user interface.

33 |
34 |

Difficulty Level Explanation

35 |
Beginner
36 |

37 | DVGA's Beginner level is literally the default GraphQL implementation without any restrictions, security controls, or other protections. This is what you would get out of the box in most of the GraphQL implementations without hardening, with the addition of other custom vulnerabilities. 38 |

39 |
Hard
40 |

DVGA's Hard level is a hardened GraphQL implementation which contains a few security controls against malicious queries, such as Cost Based Analysis, Query Depth, Field De-dup checks, etc.

41 |
42 |

GraphQL Resources

43 |

44 | To learn about GraphQL, and common GraphQL weaknesses and attacks, the following 45 | resources may be beneficial: 46 |

47 |
  Videos
48 | 59 | 60 |
  Articles
61 | 69 |
70 |

Got Stuck?

71 |

72 | Head over to the Solutions page to reveal 73 | the challenge answers. 74 |

75 |
76 |

Bug Reporting

77 |

78 | Found a bug? submit an issue on 79 | GitHub. 80 |

81 |
82 |
83 | 84 | {% endblock %} 85 | 86 | {% block scripts %} 87 | {% include 'partials/base_scripts.html' %} 88 | {% endblock %} 89 | -------------------------------------------------------------------------------- /templates/partials/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block header %} 6 | {% endblock %} 7 | 8 | 9 | 10 |
11 | {% include 'partials/sidebar.html' %} 12 | 13 | 14 | {% block content %} 15 | {% endblock %} 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% block scripts %} 25 | {% endblock %} 26 | 27 | 28 | -------------------------------------------------------------------------------- /templates/partials/base_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Damn Vulnerable GraphQL Application 10 | 11 | 12 | -------------------------------------------------------------------------------- /templates/partials/base_scripts.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/partials/navbar.html: -------------------------------------------------------------------------------- 1 |
2 | 49 |
50 | -------------------------------------------------------------------------------- /templates/partials/pastes/create_paste.html: -------------------------------------------------------------------------------- 1 |

Create a Paste

2 |
3 |
4 |
5 |
6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 | 38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
-------------------------------------------------------------------------------- /templates/partials/pastes/import_paste.html: -------------------------------------------------------------------------------- 1 |

Import a Paste

2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /templates/partials/pastes/my_pastes.html: -------------------------------------------------------------------------------- 1 |

Private Pastes

2 | 3 |
4 | -------------------------------------------------------------------------------- /templates/partials/pastes/public_pastes.html: -------------------------------------------------------------------------------- 1 |

Public Pastes

2 | 3 |
4 | -------------------------------------------------------------------------------- /templates/partials/pastes/upload_paste.html: -------------------------------------------------------------------------------- 1 |

Upload a Paste

2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 18 |
19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /templates/partials/sidebar.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_1.html: -------------------------------------------------------------------------------- 1 | 2 |

Denial of Service :: Batch Query Attack

3 |
4 |
Problem Statement
5 |

GraphQL supports Request Batching. Batched requests are processed one after the other by GraphQL, which makes it a good candidate for Denial of Service attacks, as well as other attacks such as Brute Force and Enumeration.

6 |

If a resource intensive GraphQL query is identified, an attacker may leverage batch processing to call the query and potentially overwhelm the service for a prolonged period of time.

7 |

The query systemUpdate seems to be taking a long time to complete, and can be used to overwhelm the server by batching a system update request query.

8 |
Resources
9 | 13 | 14 |
Exploitation Solution
15 | 32 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_10.html: -------------------------------------------------------------------------------- 1 | 2 |

Code Execution :: OS Command Injection #1

3 |
4 |
Problem Statement
5 |

6 | The mutation importPaste allows escaping from the parameters and introduce a UNIX command by chaining 7 | commands. The GraphQL resolver does not sufficiently validate the input, and passes it directly 8 | into cURL.

9 |
Resources
10 | 17 |
Exploitation Solution
18 | 38 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_11.html: -------------------------------------------------------------------------------- 1 | 2 |

Code Execution :: OS Command Injection #2

3 |
4 |
Problem Statement
5 |

6 | The query systemDiagnostics accepts certain UNIX binaries as parameters for debugging purposes, such as 7 | whoami, ps, etc. It acts as a restricted shell. However, it is protected 8 | with a username and password. After obtaining the correct 9 | credentials, the restricted shell seems to be bypassable by chaining commands together. 10 |

11 |
Resources
12 | 19 |
Exploitation Solution
20 | 47 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_12.html: -------------------------------------------------------------------------------- 1 | 2 |

Injection :: Stored Cross Site Scripting

3 |
4 |
Problem Statement
5 |

6 | The GraphQL mutations createPaste and importPaste allow creating and importing new pastes. The pastes may include any character without any restrictions. The pastes would then render in 7 | the Public and Private paste pages, which would result in a Cross Site Scripting vulnerability (XSS).

8 |
Resources
9 | 16 |
Exploitation Solution
17 | 33 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_13.html: -------------------------------------------------------------------------------- 1 | 2 |

Injection :: Log Injection // Log Spoofing

3 |
4 |
Problem Statement
5 |

6 | GraphQL actions such as mutation and query have the ability to take an operation name as part of the query. 7 | Here is an example query that uses MyName as an operation name: 8 |

query MyName {
 9 |     getMyName
10 |     {
11 |       first
12 |       last
13 |     }
14 |   } 

15 |

The application is keeping track of all queries and mutations users are executing on this system in order to display them in the audit log.

16 |

However, the application is not doing a fair job at verifying the operation name.

17 |
Resources
18 | 25 |
Exploitation Solution
26 | 48 | 49 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_14.html: -------------------------------------------------------------------------------- 1 | 2 |

Injection :: HTML Injection

3 |
4 |
Problem Statement
5 |

6 | Similarly to the Cross Site Scripting problem, a paste can also include HTML tags that would render in the application, resulting in an HTML injection. 7 |

8 |
Resources
9 | 16 |
Exploitation Solution
17 | 34 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_15.html: -------------------------------------------------------------------------------- 1 | 2 |

Injection :: SQL Injection

3 |
4 |
Problem Statement
5 |

6 | Pastes can be filtered using the filter parameter and it allows sending raw strings as query filters which are prone to SQL injections. 7 |

8 |
Resources
9 | 16 |
Exploitation Solution
17 | 28 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_16.html: -------------------------------------------------------------------------------- 1 | 2 |

Authorization Bypass :: GraphQL Interface Protection Bypass

3 |
4 |
Problem Statement
5 |

6 | GraphiQL is available at the path /graphiql with a poorly implemented authorization check. 7 |

8 |
Resources
9 | 16 |
Exploitation Solution
17 | 32 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_17.html: -------------------------------------------------------------------------------- 1 | 2 |

Authorization Bypass :: GraphQL Query Deny List Bypass

3 |
4 |
Problem Statement
5 |

6 | Creating an allow-list or deny-list for GraphQL is a common technique to prevent malicious queries from 7 | being resolved by GraphQL. 8 |

9 | 19 |

20 | In general, the allow-list approach is easier to maintain and less error-prone, since we only allow certain things we 21 | trust. It does not mean it cannot have flaws too. 22 |

23 |

24 | The application has a deny-list mechanism implemented that attempts to reject Health queries using the 25 | systemHealth query. 26 |

27 |

28 | The problem with this mechanism is that it does not take into consideration queries can have operation names. 29 |

30 |
Resources
31 | 43 |
Exploitation Solution
44 | 52 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_18.html: -------------------------------------------------------------------------------- 1 | 2 |

Miscellaneous :: Arbitrary File Write // Path Traversal

3 |
4 |
Problem Statement
5 |

6 | The mutation uploadPaste allows uploading pastes from the user's computer by specifying the file along 7 | with the filename. The pastes are then stored on the server under a dedicated folder. The filename 8 | argument allows any string, effectively providing the ability to write the file to any location on the server's 9 | filesystem by traversing folders using ../../ 10 |

11 |
Resources
12 | 19 |
Exploitation Solution
20 | 29 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_19.html: -------------------------------------------------------------------------------- 1 | 2 |

Miscellaneous :: GraphQL Query Weak Password Protection

3 |
4 |
Problem Statement
5 |

6 | The query systemDiagnostics is an administrative functionality that allows running a subset of system 7 | commands on the server. The query is governed by a username and password before processing the 8 | command. 9 |

10 |

11 | The password is weak, and the server has no rate limiting protections. This allows attackers to easily conduct brute 12 | force attacks against the server.

13 |
Resources
14 | 21 |
Exploitation Solution
22 | 37 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_2.html: -------------------------------------------------------------------------------- 1 | 2 |

Denial of Service :: Deep Recursion Query Attack

3 |
4 |
Problem Statement
5 |

In GraphQL, when types reference eachother, it is often possible to build a circular query that grows exponentially to a point it could bring the server down to its knees. Countermeasures such as max_depth can help mitigate these types of attacks.

6 |

The max_depth functionality acts as a safeguard, and defines how deep a query can get, ensuring deeply constructed queries will not be accepted by GraphQL.

7 |

The application offers two types, namely Owner and Paste, which reference eachother (an owner has a paste, and a paste has an owner), allowing a recursive query to be executed successfully. 8 |

Resources
9 | 21 | 22 |
Exploitation Solution
23 | 59 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_20.html: -------------------------------------------------------------------------------- 1 | 2 |

Denial of Service :: Circular Fragment

3 |
4 |
Problem Statement
5 |

6 | The GraphQL API allows creating circular fragments, such that two fragments are cross-referencing eachother. 7 | When a Spread Operator (...) references a fragment, which in return references a 2nd fragment that leads to the former fragment, may cause a recursive loop and crash the server. 8 |

9 |
Resources
10 | 22 |
Exploitation Solution
23 | 42 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_21.html: -------------------------------------------------------------------------------- 1 | 2 |

Information Disclosure :: Stack Trace Errors

3 |
4 |
Problem Statement
5 |

6 | The dedicated GraphiQL API endpoint /graphiql throws stack traces and debugging messages upon erroneous queries. 7 |

8 |
Exploitation Solution
9 | 20 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_22.html: -------------------------------------------------------------------------------- 1 | 2 |

Authorization Bypass :: GraphQL JWT Token Forge

3 |
4 |
Problem Statement
5 |

6 | Without logging in a user is able to forge the user identity claim within the JWT token for the me query operation. 7 |

8 |
Exploitation Solution
9 | 21 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_3.html: -------------------------------------------------------------------------------- 1 | 2 |

Denial of Service :: Resource Intensive Query Attack

3 |
4 |
Problem Statement
5 |

Sometimes, certain queries may be computationally more expensive than others. A query may include certain fields that would trigger more complex backend logic in order to fulfill the query resolution. As attackers, 6 | we can abuse it by calling these actions frequently in order to cause resource exhaustion.

7 |

In GraphQL, a concept called Query Cost Analysis exists, which assigns weight values to fields that are more expensive to resolve than others. Using this feature, we can then create an upper threshold to reject 8 | queries that are expensive. Alternatively, a cache feature can be implemented to avoid repeating the same request in a short time window.

9 |
Resources
10 | 17 |
Exploitation Solution
18 | 39 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_4.html: -------------------------------------------------------------------------------- 1 | 2 |

Denial of Service :: Field Duplication Attack

3 |
4 |
Problem Statement
5 |

Various GraphQL implementation do not bother de-duplicating repeating fields in GraphQL, allowing the user to multiply the same requested fields as they wish.

6 |

This causes extra load on the server to return the same fields over and over again.

7 |

There are a few ways in which this issue can be mitigated:

8 |

9 |

13 |

14 |

Field De-Duplication can be achieved by using a middleware function to traverse the schema and remove any duplications, or simply analyze repeating patterns in order to reject the query.

15 |

Query Cost Analysis will be beneficial against these attacks, since each field will ultimately result in increased cost.

16 |
Resources
17 | 24 |
Exploitation Solution
25 | 43 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_5.html: -------------------------------------------------------------------------------- 1 | 2 |

Denial of Service :: Aliases based Attack

3 |
4 |
Problem Statement
5 |

In GraphQL, it is possible to run multiple queries without needing to batch them together.

6 |

If batching is disabled, you could build a query composed of multiple aliases calling the same query or mutation, if the server is not analyzing the cost of the query, it will be possible to overwhelm the server's resources by using expensive queries using aliases.

7 |

8 |

12 |

13 |

Query Middleware is needed to identify the use of aliases in order to make a decision (reject/allow) based on your business logic.

14 |

Query Cost Analysis will be beneficial against these attacks, since each query will ultimately result in increased cost.

15 |
Resources
16 | 28 |
Exploitation Solution
29 | 40 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_6.html: -------------------------------------------------------------------------------- 1 | 2 |

Information Disclosure :: GraphQL Introspection

3 |
4 |
Problem Statement
5 |

GraphQL Introspection is a special query that uses the __schema field to interrogate GraphQL for its schema.

6 |

7 | Introspection in itself is not a weakness, but a feature. However, if it is made available, it can be used and abused by attackers seeking information about your GraphQL implementation, such as what queries or mutations exist. 8 |

9 |

10 | It is recommended to disable introspection in production to avoid data leakages. 11 |

12 |

13 | Note: If introspection query is disabled, attackers may fall back to using the Field Suggestion feature to understand what queries and fields are supported by your GraphQL. Refer to Information Disclosure :: GraphQL Field Suggestionsattack for more information. 14 |

15 | 16 |
Resources
17 | 44 |
Exploitation Solution
45 | 65 | 66 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_7.html: -------------------------------------------------------------------------------- 1 | 2 |

Information Disclosure :: GraphQL Interface

3 |
4 |
Problem Statement
5 |

6 | GraphQL has a an Integrated Development Environment named GraphiQL (note the i) that allows constructing queries in a friendly user interface. 7 |

8 |

9 | GraphiQL is usually found in paths such as: /graphiql or /console, however, it can be in other places too. 10 |

11 |
Resources
12 | 24 |
Exploitation Solution
25 | 35 | 36 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_8.html: -------------------------------------------------------------------------------- 1 | 2 |

Information Disclosure :: GraphQL Field Suggestions

3 |
4 |
Problem Statement
5 |

6 | GraphQL has a feature for field and operation suggestions. When a developer wants to integrate with a GraphQL API and types an incorrect field, as an example, GraphQL will attempt to suggest nearby fields that are 7 | similar. 8 |

9 |

10 | Field suggestions is not a vulnerability in itself, but a feature that can be abused to gain more insight into GraphQL's schema, especially when Introspection is not allowed. 11 |

12 | 13 |
Resources
14 | 36 |
Exploitation Solution
37 | 61 | -------------------------------------------------------------------------------- /templates/partials/solutions/solution_9.html: -------------------------------------------------------------------------------- 1 | 2 |

Information Disclosure :: Server Side Request Forgery

3 |
4 |
Problem Statement
5 |

6 | The GraphQL mutation importPaste accepts arbitrary host, port and scheme to import pastes from and does 7 | not restrict input such as localhost or other internal servers from being used. This may allow 8 | forging requests on behalf of the application server to target other network nodes.

9 |
Resources
10 | 22 |
Exploitation Solution
23 | 34 | -------------------------------------------------------------------------------- /templates/paste.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/base.html' %} 2 | 3 | {% block header %} 4 | {% include 'partials/base_header.html' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | {% include 'partials/navbar.html' %} 11 |
12 |
13 | {% if page == "create_paste" %} 14 | {% include 'partials/pastes/create_paste.html' %} 15 | {% elif page == "import_paste" %} 16 | {% include 'partials/pastes/import_paste.html' %} 17 | {% elif page == "upload_paste" %} 18 | {% include 'partials/pastes/upload_paste.html' %} 19 | {% elif page == "my_pastes" %} 20 | {% include 'partials/pastes/my_pastes.html' %} 21 | {% elif page == "public_pastes" %} 22 | {% include 'partials/pastes/public_pastes.html' %} 23 | {% endif %} 24 |
25 |
26 | {% endblock %} 27 | 28 | {% block scripts %} 29 | 88 | {% include 'partials/base_scripts.html' %} 89 | 343 | {% endblock %} 344 | -------------------------------------------------------------------------------- /templates/solutions.html: -------------------------------------------------------------------------------- 1 | {% extends 'partials/base.html' %} 2 | 3 | {% block header %} 4 | {% include 'partials/base_header.html' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | {% include 'partials/navbar.html' %} 11 |
12 |

Challenge Solutions

13 |
14 |
15 |
16 |
17 |
Solutions
18 |
19 |
20 |

Table of Contents

21 | 131 |
132 |
133 | 134 |
135 |

Overview

136 |
137 |

Legend

138 |
    139 |
  • - GraphQL Official Documentation / Blog Posts
  • 140 |
  • - Code Snippet
  • 141 |
  • - GraphQL Security Utility
  • 142 |
  • - Published Vulnerability (H1, CVE, etc.)
  • 143 |
144 | 145 |

146 |   There may be more than one way to exploit any given vulnerability, the solutions demonstrated below aim to illustrate one way of achieving successful exploitation. 147 |
148 |   Some solutions include code snippets that are written in Python and use the requests library for HTTP requests. 149 |

150 | 151 |

Getting Started

152 |
153 |

The first essential step in every security test is to gain a bit of insight into the technology the remote server is using. By knowing the technologies in use, you can start building up a plan how to attack the application or the underlying infrastructure.

154 |

For GraphQL, a tool called graphw00f exists. Let's explore how it can help us achieve detection and fingerprinting of GraphQL.

155 |

Detecting GraphQL

156 |
157 |

Detecting where GraphQL lives is pretty trivial, there are common places where you would typically see a graphql endpoint. For example, /graphql, /v1/graphql, etc.

158 |

Point graphw00f at DVGA to figure out where GraphQL lives:

159 |

160 | $>  python3 graphw00f.py -d -t http://localhost:5013/graphql
161 |                 +-------------------+
162 |                 |     graphw00f     |
163 |                 +-------------------+
164 |                   ***            ***
165 |                 **                  ***
166 |               **                       **
167 |     +--------------+              +--------------+
168 |     |    Node X    |              |    Node Y    |
169 |     +--------------+              +--------------+
170 |                   ***            ***
171 |                      **        **
172 |                        **    **
173 |                     +------------+
174 |                     |   Node Z   |
175 |                     +------------+
176 | 
177 |                 graphw00f - v1.0.3
178 |           The fingerprinting tool for GraphQL
179 |            Dolev Farhi (dolev@lethalbit.com)
180 | 
181 | Checking http://dvga.example.local:5013/graphql
182 | [*] Found GraphQL at http://dvga.example.local:5013/graphql
183 | [*] You can now try and fingerprint GraphQL using: graphw00f.py -t http://dvga.example.local:5013/graphql
184 | 
185 |

186 | 187 |

Fingerprinting GraphQL

188 |
189 |

graphw00f can try and fingerprint GraphQL servers in order to determine the underlying implementation. By knowing what specific engine runs GraphQL, you can map what security mechanisms you may face during an assessment.

190 |

Point graphw00f at DVGA to figure out what technology it's running.

191 |

192 | $> python3 graphw00f.py -t http://dvga.example.local:5013/graphql -f
193 | 
194 | [*] Checking if GraphQL is available at http://dvga.example.local:5013/graphql...
195 | [*] Found GraphQL...
196 | [*] Attempting to fingerprint...
197 | [*] Discovered GraphQL Engine: (Graphene)
198 | [!] Attack Surface Matrix: https://github.com/dolevf/graphw00f/blob/main/docs/graphene.md
199 | [!] Technologies: Python
200 | [!] Homepage: https://graphene-python.org
201 | [*] Completed.
202 |               
203 |

204 |

As you can see, DVGA runs graphene. Use the Attack Surface Matrix to see how Graphene ships GrapQL by default from a security perspective.

205 |
206 | {% for solution in solutions %} 207 | {% include solution %} 208 |
209 | {% endfor %} 210 |
211 |
212 |
213 |
214 |
215 |
216 | 217 | {% endblock %} 218 | 219 | {% block scripts %} 220 | {% include 'partials/base_scripts.html' %} 221 | 231 | {% endblock %} -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | import uuid 4 | 5 | from os import environ 6 | 7 | IP = environ.get('WEB_HOST', '127.0.0.1') 8 | PORT = environ.get('WEB_PORT', 5013) 9 | 10 | URL = 'http://{}:{}'.format(IP, PORT) 11 | GRAPHQL_URL = URL + '/graphql' 12 | GRAPHIQL_URL = URL + '/graphiql' 13 | 14 | def generate_id(): 15 | return str(uuid.uuid4())[4] 16 | 17 | def graph_query(url, query=None, operation="query", headers={}): 18 | return requests.post(url, 19 | verify=False, 20 | allow_redirects=True, 21 | timeout=30, 22 | headers=headers, 23 | json={operation:query}) -------------------------------------------------------------------------------- /tests/test_args_and_directives.py: -------------------------------------------------------------------------------- 1 | from common import GRAPHQL_URL, graph_query 2 | 3 | def test_capitalize_field_argument(): 4 | query = ''' 5 | query { 6 | users { 7 | username(capitalize: true) 8 | } 9 | } 10 | ''' 11 | r = graph_query(GRAPHQL_URL, query) 12 | 13 | assert r.json()['data']['users'][0]['username'] in ('Admin', 'Operator') 14 | 15 | def test_show_network_directive(): 16 | query = ''' 17 | query { 18 | pastes { 19 | ipAddr @show_network(style:"cidr") 20 | } 21 | } 22 | ''' 23 | r = graph_query(GRAPHQL_URL, query) 24 | 25 | assert r.json()['data']['pastes'][0]['ipAddr'].endswith('/32') 26 | 27 | query = ''' 28 | query { 29 | pastes { 30 | ipAddr @show_network(style:"netmask") 31 | } 32 | } 33 | ''' 34 | r = graph_query(GRAPHQL_URL, query) 35 | 36 | assert r.json()['data']['pastes'][0]['ipAddr'].startswith('255.255.') -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import GRAPHQL_URL, graph_query 4 | 5 | def test_mutation_login_success(): 6 | query = ''' 7 | mutation { 8 | login(username: "operator", password:"password123") { 9 | accessToken 10 | } 11 | } 12 | ''' 13 | r = graph_query(GRAPHQL_URL, query) 14 | 15 | assert r.json()['data']['login']['accessToken'] 16 | 17 | 18 | def test_mutation_login_error(): 19 | query = ''' 20 | mutation { 21 | login(username: "operator", password:"dolevwashere") { 22 | accessToken 23 | } 24 | } 25 | ''' 26 | r = graph_query(GRAPHQL_URL, query) 27 | 28 | assert r.json()['errors'][0]['message'] == 'Authentication Failure' 29 | 30 | 31 | def test_query_me(): 32 | query = ''' 33 | query { 34 | me(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjU2ODE0OTQ4LCJuYmYiOjE2NTY4MTQ5NDgsImp0aSI6ImI5N2FmY2QwLTUzMjctNGFmNi04YTM3LTRlMjdjODY5MGE2YyIsImlkZW50aXR5IjoiYWRtaW4iLCJleHAiOjE2NTY4MjIxNDh9.-56ZQN9jikpuuhpjHjy3vLvdwbtySs0mbdaSq-9RVGg") { 35 | id 36 | username 37 | password 38 | } 39 | } 40 | ''' 41 | 42 | r = graph_query(GRAPHQL_URL, query) 43 | 44 | assert r.json()['data']['me']['id'] == '1' 45 | assert r.json()['data']['me']['username'] == 'admin' 46 | assert r.json()['data']['me']['password'] == 'changeme' 47 | 48 | 49 | def test_query_me_operator(): 50 | query = ''' 51 | query { 52 | me(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjU2ODE0OTQ4LCJuYmYiOjE2NTY4MTQ5NDgsImp0aSI6ImI5N2FmY2QwLTUzMjctNGFmNi04YTM3LTRlMjdjODY5MGE2YyIsImlkZW50aXR5Ijoib3BlcmF0b3IiLCJleHAiOjE2NTY4MjIxNDh9.iZ-Sifz1WEkcy1CwX4c-rzI-QgfzUMqpWr2oYr8vZ1o") { 53 | id 54 | username 55 | password 56 | } 57 | } 58 | ''' 59 | 60 | r = graph_query(GRAPHQL_URL, query) 61 | 62 | assert r.json()['data']['me']['id'] == '2' 63 | assert r.json()['data']['me']['username'] == 'operator' 64 | assert r.json()['data']['me']['password'] == '******' -------------------------------------------------------------------------------- /tests/test_batching.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL, GRAPHQL_URL, graph_query 4 | 5 | def test_batching(): 6 | queries = [ 7 | {"query":"query BATCH_ABC { pastes { title } }"}, 8 | {"query":"query BATCH_DEF { pastes { content } }"} 9 | ] 10 | 11 | r = requests.post(GRAPHQL_URL, json=queries) 12 | assert r.status_code == 200 13 | assert isinstance(r.json(), list) 14 | assert len(r.json()) == 2 15 | for i in r.json(): 16 | for paste in i['data']['pastes']: 17 | for field in paste.keys(): 18 | assert field in ('title', 'content') 19 | 20 | def test_batched_operation_names(): 21 | r = requests.get(URL + '/audit') 22 | assert r.status_code == 200 23 | assert 'BATCH_ABC' in r.text 24 | assert 'BATCH_DEF' in r.text 25 | -------------------------------------------------------------------------------- /tests/test_cookies.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL 4 | 5 | def test_check_graphiql_cookie(): 6 | r = requests.get(URL + '/') 7 | assert r.status_code == 200 8 | assert 'env=graphiql:disable' in r.headers.get('Set-Cookie') 9 | 10 | -------------------------------------------------------------------------------- /tests/test_graphiql.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import GRAPHIQL_URL 4 | 5 | def test_check_batch_disabled(): 6 | query = """ 7 | query { 8 | __typename 9 | } 10 | """ 11 | r = requests.post(GRAPHIQL_URL, verify=False, allow_redirects=True, timeout=4, json=[{"query":query}]) 12 | assert not isinstance(r.json(), list) 13 | assert r.json()['errors'][0]['message'] == 'Batch GraphQL requests are not enabled.' 14 | -------------------------------------------------------------------------------- /tests/test_graphql.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import GRAPHQL_URL 4 | 5 | def test_check_batch_enabled(): 6 | query = """ 7 | query { 8 | __typename 9 | } 10 | """ 11 | r = requests.post(GRAPHQL_URL, verify=False, allow_redirects=True, timeout=4, json=[{"query":query}]) 12 | assert isinstance(r.json(), list) 13 | -------------------------------------------------------------------------------- /tests/test_intialize.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | from common import URL, GRAPHIQL_URL, GRAPHQL_URL 5 | 6 | """ 7 | DVGA Sanity Check 8 | """ 9 | def test_dvga_is_up(): 10 | """Checks DVGA UI HTML returns correct information""" 11 | r = requests.get(URL) 12 | assert 'Damn Vulnerable GraphQL Application' in r.text 13 | 14 | def test_graphql_endpoint_up(): 15 | """Checks /graphql is up""" 16 | r = requests.get(GRAPHQL_URL) 17 | assert "Must provide query string." in r.json()['errors'][0]['message'] 18 | 19 | def test_graphiql_endpoint_up(): 20 | """Checks /graphiql is up""" 21 | r = requests.get(GRAPHIQL_URL) 22 | assert "Must provide query string." in r.json()['errors'][0]['message'] 23 | -------------------------------------------------------------------------------- /tests/test_introspect.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL, GRAPHQL_URL, graph_query 4 | 5 | def test_check_introspect_fields(): 6 | fields = ['pastes', 'paste', 'systemUpdate', 'systemDiagnostics', 'systemDebug', 'systemHealth', 'users', 'readAndBurn', 'search', 'audits', 'deleteAllPastes', 'me'] 7 | r = requests.get(URL + '/difficulty/easy') 8 | assert r.status_code == 200 9 | 10 | query = """ 11 | query { 12 | __schema { 13 | queryType { 14 | fields { 15 | name 16 | } 17 | } 18 | } 19 | } 20 | """ 21 | r = graph_query(GRAPHQL_URL, query) 22 | 23 | for field in r.json()['data']['__schema']['queryType']['fields']: 24 | field_name = field['name'] 25 | assert field_name in fields 26 | assert not field_name not in fields 27 | fields.remove(field_name) 28 | 29 | assert len(fields) == 0 30 | 31 | def test_check_introspect_when_expert_mode(): 32 | query = """ 33 | query { 34 | __schema { 35 | __typename 36 | } 37 | } 38 | """ 39 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Expert'}) 40 | assert r.status_code == 200 41 | assert r.json()['errors'][0]['message'] == '400 Bad Request: Introspection is Disabled' 42 | 43 | 44 | def test_check_introspect_mutations(): 45 | fields = ['createUser', 'createPaste', 'editPaste', 'login', 'uploadPaste', 'importPaste', 'deletePaste'] 46 | r = requests.get(URL + '/difficulty/easy') 47 | assert r.status_code == 200 48 | 49 | query = """ 50 | query { 51 | __schema { 52 | mutationType { 53 | fields { 54 | name 55 | } 56 | } 57 | } 58 | } 59 | """ 60 | r = graph_query(GRAPHQL_URL, query) 61 | 62 | for field in r.json()['data']['__schema']['mutationType']['fields']: 63 | field_name = field['name'] 64 | assert field_name in fields 65 | assert not field_name not in fields 66 | fields.remove(field_name) 67 | 68 | assert len(fields) == 0 -------------------------------------------------------------------------------- /tests/test_mode.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL, GRAPHQL_URL, graph_query 4 | 5 | def test_check_hardened_mode(): 6 | r = requests.get(URL + '/difficulty/hard') 7 | assert r.status_code == 200 8 | 9 | query = """ 10 | query { 11 | __schema { 12 | __typename 13 | } 14 | } 15 | """ 16 | r = graph_query(GRAPHQL_URL, query) 17 | assert r.json()['errors'][0]['message'] == '400 Bad Request: Introspection is Disabled' 18 | 19 | def test_check_easy_mode(): 20 | r = requests.get(URL + '/difficulty/easy') 21 | assert r.status_code == 200 22 | 23 | query = """ 24 | query { 25 | __schema { 26 | __typename 27 | } 28 | } 29 | """ 30 | r = graph_query(GRAPHQL_URL, query) 31 | assert r.json()['data']['__schema']['__typename'] == '__Schema' 32 | -------------------------------------------------------------------------------- /tests/test_mutations.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL, GRAPHQL_URL, graph_query 4 | 5 | def test_mutation_createPaste(): 6 | query = ''' 7 | mutation { 8 | createPaste(burn: false, title:"Integration Test", content:"Test", public: false) { 9 | paste { 10 | burn 11 | title 12 | content 13 | public 14 | owner { 15 | id 16 | name 17 | } 18 | } 19 | } 20 | } 21 | ''' 22 | r = graph_query(GRAPHQL_URL, query) 23 | 24 | assert r.json()['data']['createPaste']['paste']['burn'] == False 25 | assert r.json()['data']['createPaste']['paste']['title'] == 'Integration Test' 26 | assert r.json()['data']['createPaste']['paste']['content'] == 'Test' 27 | assert r.json()['data']['createPaste']['paste']['public'] == False 28 | assert r.json()['data']['createPaste']['paste']['owner']['id'] 29 | assert r.json()['data']['createPaste']['paste']['owner']['name'] 30 | 31 | def test_mutation_editPaste(): 32 | query = ''' 33 | mutation { 34 | editPaste(id: 1, title:"Integration Test123", content:"Integration Test456") { 35 | paste { 36 | id 37 | title 38 | content 39 | userAgent 40 | burn 41 | ownerId 42 | owner { 43 | id 44 | name 45 | } 46 | } 47 | } 48 | } 49 | ''' 50 | r = graph_query(GRAPHQL_URL, query) 51 | 52 | assert r.json()['data']['editPaste']['paste']['id'] == '1' 53 | assert r.json()['data']['editPaste']['paste']['title'] == 'Integration Test123' 54 | assert r.json()['data']['editPaste']['paste']['content'] == 'Integration Test456' 55 | assert r.json()['data']['editPaste']['paste']['userAgent'] 56 | assert r.json()['data']['editPaste']['paste']['burn'] == False 57 | assert r.json()['data']['editPaste']['paste']['ownerId'] 58 | assert r.json()['data']['editPaste']['paste']['owner']['id'] == '1' 59 | assert r.json()['data']['editPaste']['paste']['owner']['name'] 60 | 61 | def test_mutation_deletePaste(): 62 | query = ''' 63 | mutation { 64 | deletePaste(id: 91000) { 65 | result 66 | } 67 | } 68 | ''' 69 | r = graph_query(GRAPHQL_URL, query) 70 | 71 | assert r.json()['data']['deletePaste']['result'] == False 72 | 73 | query = ''' 74 | mutation { 75 | deletePaste(id: 5) { 76 | result 77 | } 78 | } 79 | ''' 80 | r = graph_query(GRAPHQL_URL, query) 81 | 82 | assert r.json()['data']['deletePaste']['result'] == True 83 | 84 | def test_mutation_uploadPaste(): 85 | query = ''' 86 | mutation { 87 | uploadPaste(content:"Uploaded Content", filename:"test.txt") { 88 | result 89 | } 90 | } 91 | ''' 92 | r = graph_query(GRAPHQL_URL, query) 93 | 94 | assert r.json()['data']['uploadPaste']['result'] == "Uploaded Content" 95 | 96 | query = ''' 97 | query { 98 | pastes { 99 | content 100 | } 101 | } 102 | ''' 103 | r = graph_query(GRAPHQL_URL, query) 104 | 105 | found = False 106 | for i in r.json()['data']['pastes']: 107 | if i['content'] == 'Uploaded Content': 108 | found = True 109 | 110 | assert found == True 111 | 112 | 113 | def test_mutation_importPaste(): 114 | query = ''' 115 | mutation { 116 | importPaste(scheme: "https", host:"icanhazip.com", path:"/", port:443) { 117 | result 118 | } 119 | } 120 | ''' 121 | r = graph_query(GRAPHQL_URL, query) 122 | 123 | assert r.json()['data']['importPaste']['result'] 124 | assert '.' in r.json()['data']['importPaste']['result'] 125 | 126 | 127 | def test_mutation_createUser(): 128 | query = ''' 129 | mutation { 130 | createUser(userData:{username:"integrationuser", email:"test@blackhatgraphql.com", password:"strongpass"}) { 131 | user { 132 | username 133 | } 134 | } 135 | } 136 | ''' 137 | r = graph_query(GRAPHQL_URL, query) 138 | 139 | assert r.json()['data']['createUser']['user']['username'] == 'integrationuser' 140 | 141 | def test_mutation_createBurnPaste(): 142 | query = ''' 143 | mutation { 144 | createPaste(burn: true, content: "Burn Me", title: "Burn Me", public: true) { 145 | paste { 146 | content 147 | burn 148 | title 149 | id 150 | } 151 | } 152 | } 153 | ''' 154 | r = graph_query(GRAPHQL_URL, query) 155 | 156 | assert r.status_code == 200 157 | assert r.json()['data']['createPaste']['paste']['content'] == 'Burn Me' 158 | assert r.json()['data']['createPaste']['paste']['title'] == 'Burn Me' 159 | assert r.json()['data']['createPaste']['paste']['id'] 160 | 161 | paste_id = r.json()['data']['createPaste']['paste']['id'] 162 | 163 | query = ''' 164 | query { 165 | readAndBurn(id: %s) { 166 | content 167 | burn 168 | title 169 | id 170 | } 171 | } 172 | ''' % paste_id 173 | 174 | r = graph_query(GRAPHQL_URL, query) 175 | 176 | assert r.status_code == 200 177 | assert r.json()['data']['readAndBurn']['content'] == 'Burn Me' 178 | assert r.json()['data']['readAndBurn']['title'] == 'Burn Me' 179 | assert r.json()['data']['readAndBurn']['id'] 180 | 181 | 182 | query = ''' 183 | query { 184 | readAndBurn(id: %s) { 185 | content 186 | burn 187 | title 188 | id 189 | } 190 | } 191 | ''' % paste_id 192 | r = graph_query(GRAPHQL_URL, query) 193 | 194 | assert r.status_code == 200 195 | assert r.json()['data']['readAndBurn'] == None 196 | 197 | -------------------------------------------------------------------------------- /tests/test_queries.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL, GRAPHQL_URL, graph_query 4 | 5 | def test_query_pastes(): 6 | query = ''' 7 | query { 8 | pastes { 9 | id 10 | ipAddr 11 | ownerId 12 | burn 13 | owner { 14 | id 15 | name 16 | } 17 | title 18 | content 19 | userAgent 20 | } 21 | } 22 | ''' 23 | r = graph_query(GRAPHQL_URL, query) 24 | 25 | assert r.json()['data']['pastes'][0]['id'] 26 | assert r.json()['data']['pastes'][0]['ipAddr'] 27 | assert r.json()['data']['pastes'][0]['ownerId'] == 1 28 | assert r.json()['data']['pastes'][0]['burn'] == False 29 | assert r.json()['data']['pastes'][0]['owner']['id'] == '1' 30 | assert r.json()['data']['pastes'][0]['owner']['name'] == 'DVGAUser' 31 | assert r.json()['data']['pastes'][0]['title'] 32 | assert r.json()['data']['pastes'][0]['userAgent'] 33 | assert r.json()['data']['pastes'][0]['content'] 34 | 35 | def test_query_paste_by_id(): 36 | query = ''' 37 | query { 38 | paste (id: 1) { 39 | id 40 | ipAddr 41 | ownerId 42 | burn 43 | owner { 44 | id 45 | name 46 | } 47 | title 48 | content 49 | userAgent 50 | } 51 | } 52 | ''' 53 | r = graph_query(GRAPHQL_URL, query) 54 | 55 | assert r.json()['data']['paste']['id'] == '1' 56 | assert r.json()['data']['paste']['ipAddr'] == '127.0.0.1' 57 | assert r.json()['data']['paste']['ownerId'] == 1 58 | assert r.json()['data']['paste']['burn'] == False 59 | assert r.json()['data']['paste']['owner']['id'] == '1' 60 | assert r.json()['data']['paste']['owner']['name'] == 'DVGAUser' 61 | assert r.json()['data']['paste']['title'] 62 | assert r.json()['data']['paste']['userAgent'] == 'User-Agent not set' 63 | assert r.json()['data']['paste']['content'] 64 | 65 | def test_query_systemHealth(): 66 | query = ''' 67 | query { 68 | systemHealth 69 | } 70 | ''' 71 | r = graph_query(GRAPHQL_URL, query) 72 | assert 'System Load' in r.json()['data']['systemHealth'] 73 | assert '.' in r.json()['data']['systemHealth'].split('System Load: ')[1] 74 | 75 | def test_query_systemUpdate(): 76 | pass 77 | 78 | def test_query_systemDebug(): 79 | query = ''' 80 | query { 81 | systemDebug 82 | } 83 | ''' 84 | r = graph_query(GRAPHQL_URL, query) 85 | assert r.status_code == 200 86 | 87 | systemdebug_indicators = ['TTY', 'COMMAND'] 88 | assert any(substring in r.json()['data']['systemDebug'] for substring in systemdebug_indicators) 89 | 90 | def test_query_users(): 91 | query = ''' 92 | query { 93 | users { 94 | id 95 | username 96 | } 97 | } 98 | ''' 99 | 100 | r = graph_query(GRAPHQL_URL, query) 101 | assert r.status_code == 200 102 | assert len(r.json()['data']['users']) > 1 103 | 104 | def test_query_users_by_id(): 105 | query = ''' 106 | query { 107 | users(id: 1) { 108 | id 109 | username 110 | } 111 | } 112 | ''' 113 | 114 | r = graph_query(GRAPHQL_URL, query) 115 | assert r.status_code == 200 116 | assert r.json()['data']['users'][0]['id'] 117 | assert len(r.json()['data']['users']) == 1 118 | 119 | 120 | def test_query_read_and_burn(): 121 | query = ''' 122 | query { 123 | readAndBurn(id: 155){ 124 | id 125 | } 126 | } 127 | ''' 128 | r = graph_query(GRAPHQL_URL, query) 129 | assert r.status_code == 200 130 | assert r.json()['data']['readAndBurn'] == None 131 | 132 | def test_query_search_on_user_object(): 133 | query = ''' 134 | query { 135 | search(keyword:"operator") { 136 | ... on UserObject { 137 | username 138 | id 139 | } 140 | } 141 | } 142 | ''' 143 | 144 | r = graph_query(GRAPHQL_URL, query) 145 | assert r.status_code == 200 146 | assert r.json()['data']['search'][0]['username'] == 'operator' 147 | assert r.json()['data']['search'][0]['id'] 148 | 149 | 150 | def test_query_search_on_paste_object(): 151 | query = ''' 152 | query { 153 | search { 154 | ... on PasteObject { 155 | owner { 156 | name 157 | id 158 | } 159 | title 160 | content 161 | id 162 | ipAddr 163 | burn 164 | ownerId 165 | } 166 | } 167 | } 168 | ''' 169 | 170 | r = graph_query(GRAPHQL_URL, query) 171 | assert r.status_code == 200 172 | assert len(r.json()['data']['search']) > 0 173 | assert r.json()['data']['search'][0]['owner']['id'] 174 | assert r.json()['data']['search'][0]['title'] 175 | assert r.json()['data']['search'][0]['content'] 176 | assert r.json()['data']['search'][0]['id'] 177 | assert r.json()['data']['search'][0]['ipAddr'] 178 | assert r.json()['data']['search'][0]['burn'] == False 179 | assert r.json()['data']['search'][0]['ownerId'] 180 | 181 | 182 | def test_query_search_on_user_and_paste_object(): 183 | query = ''' 184 | query { 185 | search(keyword: "p") { 186 | ... on UserObject { 187 | username 188 | } 189 | ... on PasteObject { 190 | title 191 | } 192 | } 193 | } 194 | ''' 195 | result = {"username":0, "title":0} 196 | 197 | r = graph_query(GRAPHQL_URL, query) 198 | assert r.status_code == 200 199 | 200 | for i in r.json()['data']['search']: 201 | if 'title' in i: 202 | result['title'] = 1 203 | elif 'username' in i: 204 | result['username'] = 1 205 | 206 | assert result['username'] == 1 207 | assert result['title'] == 1 208 | 209 | def test_query_audits(): 210 | query = ''' 211 | query { 212 | audits { 213 | id 214 | gqloperation 215 | gqlquery 216 | timestamp 217 | } 218 | } 219 | ''' 220 | 221 | r = graph_query(GRAPHQL_URL, query) 222 | assert r.status_code == 200 223 | assert len(r.json()['data']['audits']) > 0 224 | assert r.json()['data']['audits'][0]['id'] 225 | assert r.json()['data']['audits'][0]['gqloperation'] 226 | assert r.json()['data']['audits'][0]['gqlquery'] 227 | assert r.json()['data']['audits'][0]['timestamp'] 228 | 229 | def test_query_audits(): 230 | query = ''' 231 | query { 232 | deleteAllPastes 233 | } 234 | ''' 235 | 236 | r = graph_query(GRAPHQL_URL, query) 237 | assert r.status_code == 200 238 | assert r.json()['data']['deleteAllPastes'] 239 | 240 | # Rebuild 241 | r = requests.get(URL + '/start_over') 242 | assert r.status_code == 200 243 | assert 'Restored to default state' in r.text 244 | 245 | def test_query_pastes_with_limit(): 246 | query = ''' 247 | query { 248 | pastes(limit: 2, public: true) { 249 | content 250 | title 251 | owner { 252 | name 253 | } 254 | ownerId 255 | userAgent 256 | public 257 | } 258 | } 259 | ''' 260 | 261 | r = graph_query(GRAPHQL_URL, query) 262 | assert r.status_code == 200 263 | assert len(r.json()['data']['pastes']) == 2 264 | assert r.json()['data']['pastes'][0]['content'] 265 | assert r.json()['data']['pastes'][0]['title'] 266 | assert r.json()['data']['pastes'][0]['owner']['name'] 267 | assert r.json()['data']['pastes'][0]['ownerId'] 268 | assert r.json()['data']['pastes'][0]['userAgent'] 269 | assert r.json()['data']['pastes'][0]['public'] 270 | 271 | def test_query_pastes_with_fragments(): 272 | query = ''' 273 | query { 274 | pastes { 275 | ...A 276 | } 277 | } 278 | 279 | fragment A on PasteObject { 280 | content 281 | title 282 | } 283 | ''' 284 | 285 | r = graph_query(GRAPHQL_URL, query) 286 | assert r.status_code == 200 287 | assert r.json()['data']['pastes'][0]['content'] 288 | assert r.json()['data']['pastes'][0]['title'] 289 | -------------------------------------------------------------------------------- /tests/test_rollback.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL 4 | 5 | def test_check_rollback(): 6 | r = requests.get(URL + '/start_over') 7 | assert r.status_code == 200 8 | assert 'Restored to default state' in r.text 9 | -------------------------------------------------------------------------------- /tests/test_vulnerabilities.py: -------------------------------------------------------------------------------- 1 | 2 | import requests 3 | import os.path 4 | 5 | from common import graph_query, GRAPHQL_URL, GRAPHIQL_URL, URL 6 | 7 | def test_circular_query_pastes_owners(): 8 | query = """ 9 | query { 10 | pastes { 11 | owner { 12 | pastes { 13 | owner { 14 | name 15 | } 16 | } 17 | } 18 | } 19 | } 20 | """ 21 | r = graph_query(GRAPHQL_URL, query) 22 | assert r.status_code == 200 23 | assert r.json()['data']['pastes'][0]['owner']['pastes'][0]['owner']['name'] == 'DVGAUser' 24 | 25 | def test_aliases_overloading(): 26 | query = """ 27 | query { 28 | a1: pastes { id } 29 | a2: pastes { id } 30 | a3: pastes { id } 31 | a4: pastes { id } 32 | a5: pastes { id } 33 | } 34 | """ 35 | r = graph_query(GRAPHQL_URL, query) 36 | assert r.status_code == 200 37 | assert len(r.json()['data'].keys()) == 5 38 | 39 | def test_field_suggestions(): 40 | query = """ 41 | query { 42 | systemUpd 43 | } 44 | """ 45 | r = graph_query(GRAPHQL_URL, query) 46 | assert r.status_code == 400 47 | assert 'Did you mean' in r.json()['errors'][0]['message'] 48 | 49 | def test_os_injection(): 50 | query = """ 51 | mutation { 52 | importPaste(host:"hostthatdoesnotexist.com", port:80, path:"/ || id", scheme:"http") { 53 | result 54 | } 55 | } 56 | """ 57 | 58 | r = graph_query(GRAPHQL_URL, query) 59 | assert r.status_code == 200 60 | assert 'uid=' in r.json()['data']['importPaste']['result'] 61 | 62 | def test_os_injection_alt(): 63 | query = """ 64 | query { 65 | systemDiagnostics(username:"admin", password:"changeme", cmd:"id") 66 | } 67 | """ 68 | 69 | r= graph_query(GRAPHQL_URL, query) 70 | assert r.status_code == 200 71 | assert 'uid=' in r.json()['data']['systemDiagnostics'] 72 | 73 | def test_xss(): 74 | query = """ 75 | mutation { 76 | createPaste(title:"", content:"zzzz", public:true) { 77 | paste { 78 | title 79 | } 80 | } 81 | } 82 | """ 83 | 84 | r = graph_query(GRAPHQL_URL, query) 85 | assert r.status_code == 200 86 | assert r.json()['data']['createPaste']['paste']['title'] == '' 87 | 88 | def test_log_injection(): 89 | query = """ 90 | query pwned { 91 | systemHealth 92 | } 93 | """ 94 | 95 | r = graph_query(GRAPHQL_URL, query) 96 | assert r.status_code == 200 97 | r = requests.get(URL + '/audit') 98 | 99 | assert r.status_code == 200 100 | assert 'query pwned {' in r.text 101 | 102 | def test_html_injection(): 103 | query = """ 104 | mutation { 105 | createPaste(title:"

hello!

", content:"zzzz", public:true) { 106 | paste { 107 | title 108 | content 109 | public 110 | } 111 | } 112 | } 113 | """ 114 | 115 | r = graph_query(GRAPHQL_URL, query) 116 | 117 | assert r.status_code == 200 118 | assert r.json()['data']['createPaste']['paste']['title'] == '

hello!

' 119 | assert r.json()['data']['createPaste']['paste']['content'] == 'zzzz' 120 | assert r.json()['data']['createPaste']['paste']['public'] == True 121 | 122 | def test_sql_injection(): 123 | query = """ 124 | query { 125 | pastes(filter:"aaa ' or 1=1--") { 126 | content 127 | title 128 | } 129 | } 130 | """ 131 | 132 | r = graph_query(GRAPHQL_URL, query) 133 | assert r.status_code == 200 134 | assert len(r.json()['data']['pastes']) > 1 135 | 136 | def test_deny_list_expert_mode(): 137 | query = """ 138 | query { 139 | systemHealth 140 | } 141 | """ 142 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Expert'}) 143 | assert r.status_code == 200 144 | assert r.json()['errors'][0]['message'] == '400 Bad Request: Query is on the Deny List.' 145 | 146 | def test_deny_list_expert_mode_bypass(): 147 | query = """ 148 | query getPastes { 149 | systemHealth 150 | } 151 | """ 152 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Expert'}) 153 | assert r.status_code == 200 154 | assert 'System Load' in r.json()['data']['systemHealth'] 155 | assert '.' in r.json()['data']['systemHealth'].split('System Load: ')[1] 156 | 157 | def test_deny_list_beginner_mode(): 158 | query = """ 159 | query { 160 | systemHealth 161 | } 162 | """ 163 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Beginner'}) 164 | assert r.status_code == 200 165 | assert 'System Load' in r.json()['data']['systemHealth'] 166 | assert '.' in r.json()['data']['systemHealth'].split('System Load: ')[1] 167 | 168 | def test_circular_fragments(): 169 | assert os.path.exists('app.py') 170 | f = open('app.py', 'r').read() 171 | assert 'sys.setrecursionlimit(100000)' in f 172 | 173 | def test_stack_trace_errors(): 174 | query = """ 175 | query { 176 | pastes { 177 | conteeeent 178 | } 179 | } 180 | """ 181 | r = graph_query(GRAPHIQL_URL, query, headers={"X-DVGA-MODE":'Beginner'}) 182 | assert r.status_code == 400 183 | assert len(r.json()['errors'][0]['extensions']['exception']['stack']) > 0 184 | assert r.json()['errors'][0]['extensions']['exception']['stack'] 185 | assert 'Traceback' in r.json()['errors'][0]['extensions']['exception']['debug'] 186 | assert r.json()['errors'][0]['extensions']['exception']['path'].endswith('.py') 187 | -------------------------------------------------------------------------------- /tests/test_websockets.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from common import URL 4 | 5 | def test_check_websocket(): 6 | headers = { 7 | "Connection":"Upgrade", 8 | "Upgrade":"websocket", 9 | "Host":"localhost", 10 | "Origin":"localhost", 11 | "Sec-WebSocket-Version":"13", 12 | "Sec-WebSocket-Key":"+onQ3ZxjWlkNa0na6ydhNg==" 13 | } 14 | 15 | r = requests.get(URL, headers=headers) 16 | assert r.status_code == 101 17 | assert r.headers['Upgrade'] == 'websocket' 18 | assert r.headers['Connection'] == 'Upgrade' 19 | assert r.headers['Sec-WebSocket-Accept'] 20 | 21 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | VERSION = '2.2.0' 2 | --------------------------------------------------------------------------------