├── login.json ├── res ├── forking env.png └── forking collection.png ├── src ├── entrypoint.sh ├── core │ ├── constants.py │ ├── logger.py │ └── db.py ├── requirements.txt └── main.py ├── Dockerfile ├── util ├── passwords.csv ├── brute_force_jwt_token.py └── fasttrack.txt ├── postman ├── The Good Bank Environment.json └── The Good Bank Collection.json ├── .gitignore ├── README.md └── openAPISpecBank.yaml /login.json: -------------------------------------------------------------------------------- 1 | {"username":"a", "password":"a"} 2 | -------------------------------------------------------------------------------- /res/forking env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmanoj96/vulnerable-apis/HEAD/res/forking env.png -------------------------------------------------------------------------------- /res/forking collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmmanoj96/vulnerable-apis/HEAD/res/forking collection.png -------------------------------------------------------------------------------- /src/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export TRANSIENT_DB=true 3 | export USER_SALT=qzdveigybpisfxsvyzqefquouuiwvmsr 4 | python main.py -------------------------------------------------------------------------------- /src/core/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | USER_TABLE = f'users_{os.getenv("USER_SALT")}' if os.getenv("USER_SALT") else 'users_gqviprviveefkttsfbasfrpsvzutawrb' 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | ADD src/ /app 3 | ADD util/ /util 4 | WORKDIR /app 5 | RUN pip install -r requirements.txt 6 | EXPOSE 5000 7 | CMD ["./entrypoint.sh"] 8 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.0.1 2 | Flask==2.0.1 3 | itsdangerous==2.0.1 4 | Jinja2==3.0.1 5 | MarkupSafe==2.0.1 6 | PyJWT==2.1.0 7 | tqdm==4.62.3 8 | Werkzeug==2.0.1 9 | -------------------------------------------------------------------------------- /src/core/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | LOGGING_LEVEL = logging.DEBUG 4 | 5 | logging.basicConfig( 6 | level=LOGGING_LEVEL, 7 | format='%(asctime)s (%(funcName)s:%(lineno)d) - %(message)s' 8 | ) 9 | -------------------------------------------------------------------------------- /util/passwords.csv: -------------------------------------------------------------------------------- 1 | password 2 | 1qaz2wsx 3 | graham 4 | doctor 5 | advance 6 | manager 7 | darwin 8 | apache 9 | classic 10 | exim 11 | hongkong 12 | europe 13 | webalizer 14 | tomek 15 | default 16 | 123 17 | movies 18 | comerce 19 | webadmin 20 | anarchy 21 | oscar 22 | a1b2c3 23 | account 24 | tanya 25 | fudball 26 | oracle 27 | carrie 28 | charles 29 | qwaszx 30 | peter 31 | alice -------------------------------------------------------------------------------- /postman/The Good Bank Environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1e57b415-ab9e-4028-bb73-b276d28458ac", 3 | "name": "The Good Bank Environment", 4 | "values": [ 5 | { 6 | "key": "host", 7 | "value": "http://security.postman-breakable.com", 8 | "enabled": true 9 | } 10 | ], 11 | "_postman_variable_scope": "environment", 12 | "_postman_exported_at": "2021-09-23T17:32:52.863Z", 13 | "_postman_exported_using": "Postman/8.12.0" 14 | } -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # CUSTOM 132 | .dccache -------------------------------------------------------------------------------- /util/brute_force_jwt_token.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from jwt import encode, decode 3 | from json import loads, dumps 4 | from base64 import b64encode, b64decode 5 | from tqdm import tqdm 6 | 7 | def print_usage(): 8 | print('Usage:') 9 | print('\tpython3 brute_force_jwt_token.py make - to create a token using a leaked secret') 10 | print('\tpython3 brute_force_jwt_token.py break - to find the secret used by JWT token') 11 | 12 | def make_jwt(): 13 | payload = loads(input('JWT payload: ').strip()) 14 | secret = input('JWT signing secet: ').strip() 15 | algorithm = input('JWT encoding algorithm: ').strip() 16 | 17 | # EXAMPLE: uncomment the following and comment off the above to see the tool in action 18 | # payload = loads('{"username": "kmmanoj96", "expires": 9999999999.00}') 19 | # secret = 'P@55w0rd!' 20 | # algorithm = 'HS256' 21 | 22 | jwt_token = encode(payload, secret, algorithm=algorithm) 23 | print(f'JWT Token = {jwt_token}') 24 | 25 | def break_jwt(): 26 | jwt_token = input('JWT token: ') 27 | 28 | # EXAMPLE: uncomment the following (and comment off the above) to see the tool in action 29 | # jwt_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImttbWFub2o5NiIsImV4cGlyZXMiOjE2MDAwMDAwMDAuMH0.VVtGaTXKXeTuH3LaKPAnOeb0kk625QN-RYzd_ig9rkY' 30 | 31 | algorithm = None 32 | for i in range(6): 33 | try: 34 | jwt_alg_part = jwt_token.split('.')[0] 35 | algorithm = loads(b64decode(jwt_alg_part + '='*i).decode())['alg'] 36 | break 37 | except: 38 | pass 39 | payload = None 40 | for i in range(6): 41 | try: 42 | jwt_data_part = jwt_token.split('.')[1] 43 | payload = b64decode(jwt_data_part + '='*i).decode() 44 | break 45 | except: 46 | pass 47 | 48 | wordlist = input('Wordlist filepath (default: ./fasttrack.txt):') 49 | 50 | print('Breaking the JWT token:') 51 | for secret in tqdm(open('./fasttrack.txt' if wordlist == '' else wordlist).read().split('\n')): 52 | try: 53 | data = decode(jwt_token, secret, algorithms=algorithm) 54 | break 55 | except Exception: 56 | pass 57 | print(f'JWT = payload: {payload} algorithm: {algorithm} secret: {secret}') 58 | 59 | if __name__ == '__main__': 60 | if len(sys.argv) < 2: 61 | print_usage() 62 | exit(0) 63 | 64 | action = sys.argv[1] 65 | if action == 'make': 66 | make_jwt() 67 | elif action == 'break': 68 | break_jwt() 69 | else: 70 | print('Invalid action') 71 | print_usage() 72 | 73 | -------------------------------------------------------------------------------- /util/fasttrack.txt: -------------------------------------------------------------------------------- 1 | Spring2017 2 | Spring2016 3 | Spring2015 4 | Spring2014 5 | Spring2013 6 | spring2017 7 | spring2016 8 | spring2015 9 | spring2014 10 | spring2013 11 | Summer2017 12 | Summer2016 13 | Summer2015 14 | Summer2014 15 | Summer2013 16 | summer2017 17 | summer2016 18 | summer2015 19 | summer2014 20 | summer2013 21 | Autumn2017 22 | Autumn2016 23 | Autumn2015 24 | Autumn2014 25 | Autumn2013 26 | autumn2017 27 | autumn2016 28 | autumn2015 29 | autumn2014 30 | autumn2013 31 | Winter2017 32 | Winter2016 33 | Winter2015 34 | Winter2014 35 | Winter2013 36 | winter2017 37 | winter2016 38 | winter2015 39 | winter2014 40 | winter2013 41 | P@55w0rd 42 | P@ssw0rd! 43 | P@55w0rd! 44 | sqlsqlsqlsql 45 | SQLSQLSQLSQL 46 | Welcome123 47 | Welcome1234 48 | Welcome1212 49 | PassSql12 50 | network 51 | networking 52 | networks 53 | test 54 | testtest 55 | testing 56 | testing123 57 | testsql 58 | test-sql3 59 | sqlsqlsqlsqlsql 60 | bankbank 61 | default 62 | test 63 | testing 64 | password2 65 | 66 | password 67 | Password1 68 | Password1! 69 | P@ssw0rd 70 | password12 71 | Password12 72 | security 73 | security1 74 | security3 75 | secuirty3 76 | complex1 77 | complex2 78 | complex3 79 | sqlserver 80 | sql 81 | sqlsql 82 | password1 83 | password123 84 | complexpassword 85 | database 86 | server 87 | changeme 88 | change 89 | sqlserver2000 90 | sqlserver2005 91 | Sqlserver 92 | SqlServer 93 | Password1 94 | Password2 95 | P@ssw0rd 96 | P@ssw0rd! 97 | P@55w0rd! 98 | P@ssword! 99 | Password! 100 | password! 101 | sqlsvr 102 | sqlaccount 103 | account 104 | sasa 105 | sa 106 | administator 107 | pass 108 | sql 109 | microsoft 110 | sqlserver 111 | sa 112 | hugs 113 | sasa 114 | welcome 115 | welcome1 116 | welcome2 117 | march2011 118 | sqlpass 119 | sqlpassword 120 | guessme 121 | bird 122 | P@55w0rd! 123 | test 124 | dev 125 | devdev 126 | devdevdev 127 | qa 128 | god 129 | admin 130 | adminadmin 131 | admins 132 | goat 133 | sysadmin 134 | water 135 | dirt 136 | air 137 | earth 138 | company 139 | company1 140 | company123 141 | company1! 142 | company! 143 | secret 144 | secret! 145 | secret123 146 | secret1212 147 | secret12 148 | secret1! 149 | sqlpass123 150 | Summer2013 151 | Summer2012 152 | Summer2011 153 | Summer2010 154 | Summer2009 155 | Summer2008 156 | Winter2013 157 | Winter2012 158 | Winter2011 159 | Winter2010 160 | Winter2009 161 | Winter2008 162 | summer2013 163 | summer2012 164 | summer2011 165 | summer2010 166 | summer2009 167 | summer2008 168 | winter2013 169 | winter2012 170 | winter2011 171 | winter2010 172 | winter2009 173 | winter2008 174 | 123456 175 | abcd123 176 | abc 177 | burp 178 | private 179 | unknown 180 | wicked 181 | alpine 182 | trust 183 | microsoft 184 | sql2000 185 | sql2003 186 | sql2005 187 | sql2008 188 | vista 189 | xp 190 | nt 191 | 98 192 | 95 193 | 2003 194 | 2008 195 | someday 196 | sql2010 197 | sql2011 198 | sql2009 199 | complex 200 | goat 201 | changelater 202 | rain 203 | fire 204 | snow 205 | unchanged 206 | qwerty 207 | 12345678 208 | football 209 | baseball 210 | basketball 211 | abc123 212 | 111111 213 | 1qaz2wsx 214 | dragon 215 | master 216 | monkey 217 | letmein 218 | login 219 | princess 220 | solo 221 | qwertyuiop 222 | starwars 223 | -------------------------------------------------------------------------------- /src/core/db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | from core.logger import logging 4 | from core.constants import USER_TABLE 5 | 6 | class DB: 7 | @staticmethod 8 | def initialize_db(): 9 | connection = sqlite3.connect('db.sqlite3') 10 | logging.info(f'User table is not {USER_TABLE}') 11 | connection.execute(f'CREATE TABLE {USER_TABLE} (user_id string, username varchar(255), email_id varchar(255), phone varchar(255), password varchar(1023), employee int, failed_logins int, session_token varchar(1023))') 12 | connection.execute(f'CREATE TABLE account_summary (user_id string, balance real, last_transaction real)') 13 | connection.execute(f'CREATE TABLE account_transactions (user_id string, transaction_time real, transaction_party varchar(255), transaction_type varchar(7), transaction_amount real, balance real)') 14 | connection.execute(f'''INSERT INTO {USER_TABLE} VALUES 15 | ('1fd7cd4e-9925-4abf-a09d-7d0f05acb86e', 'theFounder', 'theFounder@gulbank.com', '+91 1234567890', 'th3F0und3rspassw0rd', 1, 0, ''), 16 | ('2e0b0a82-e11d-4c62-87d2-901813d684e1', 'theCEO', 'theCEO@gulbank.com', '+91 9876543210', 'th3c30sp455', 1, 0, ''), 17 | ('1ce7cb77-e991-42db-84a2-742c7e3dce16', 'user001', 'user001@email.com', '', 'userpass01isverycomplex', 0, 0, ''), 18 | ('f6938f49-d227-4745-a711-1d0616a9d6cd', 'user002', 'user002@mailid.com', '+1 1234543211', 'userpass02isalsocomplex', 0, 0, ''), 19 | ('01fb8943-ea83-4d23-94e0-80209d5a893d', 'adam', '', '', 'account', 0, 0, '') 20 | ''') 21 | connection.execute(f'''INSERT INTO account_summary VALUES 22 | ('1ce7cb77-e991-42db-84a2-742c7e3dce16', 187236.35, 1632204630.2189791), 23 | ('f6938f49-d227-4745-a711-1d0616a9d6cd', 8182.81, 163228189462.7294726), 24 | ('01fb8943-ea83-4d23-94e0-80209d5a893d', 0.23, 163228463745.9283746) 25 | ''') 26 | connection.execute(f'''INSERT INTO account_transactions VALUES 27 | ('1ce7cb77-e991-42db-84a2-742c7e3dce16', 1632204630.2189791, 'someone', 'credit', 187000.35, 187236.35), 28 | ('1ce7cb77-e991-42db-84a2-742c7e3dce16', 1632104630.8472627, 'heere', 'debit', 500.00, 236.00), 29 | ('1ce7cb77-e991-42db-84a2-742c7e3dce16', 1632004630.3857382, 'theiare', 'credit', 736.00, 736.00), 30 | ('f6938f49-d227-4745-a711-1d0616a9d6cd', 163228189462.7294726, 'somebody', 'debit', 2000, 8182.81), 31 | ('f6938f49-d227-4745-a711-1d0616a9d6cd', 163228188273.8764738, 'from here', 'credit', 4182.81, 10182.81), 32 | ('f6938f49-d227-4745-a711-1d0616a9d6cd', 163228173857.2948857, 'payed', 'credit', 4000, 6000.00), 33 | ('f6938f49-d227-4745-a711-1d0616a9d6cd', 163228171857.9284723, 'from there', 'credit', 2000, 2000.00), 34 | ('01fb8943-ea83-4d23-94e0-80209d5a893d', 163228463745.9283746, 'from nowhere', 'debit', 20000, 0.23), 35 | ('01fb8943-ea83-4d23-94e0-80209d5a893d', 163219281726.8172645, 'initially', 'credit', 20000.23, 20000.23) 36 | ''') 37 | connection.commit() 38 | connection.close() 39 | 40 | @staticmethod 41 | def retrieve(query): 42 | logging.debug(f'SELECT query {repr(query)}') 43 | connection = sqlite3.connect('db.sqlite3') 44 | cursor = connection.cursor() 45 | cursor.execute(query) 46 | results = cursor.fetchall() 47 | connection.close() 48 | return results 49 | 50 | @staticmethod 51 | def modify(query): 52 | logging.debug(f'MODIFY query {repr(query)}') 53 | connection = sqlite3.connect('db.sqlite3') 54 | cursor = connection.cursor() 55 | cursor.execute(query) 56 | connection.commit() 57 | connection.close() 58 | return True 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vulnerable-apis 2 | vulnerable APIs inspired by https://github.com/mattvaldes/vulnerable-api 3 | 4 | # Setup 5 | 6 | ## Docker 7 | ### If, Out of the box 8 | `docker pull kmmanoj/vulnerable-apis` (may be outdated with respect to the current state of the repo) 9 | 10 | ### Else, Build the application as docker image (preferred) 11 | `docker build -t kmmanoj/vulnerable-apis .` 12 | 13 | ### Finally, Run the application as docker container 14 | `docker run --name vuln-api-instance --rm -it -p 5000:5000 kmmanoj/vulnerable-apis` 15 | 16 | ## Traditional way 17 | 18 | Create a python virtual environment: `virtualenv venv` 19 | 20 | Activate the virtual environment: `source ./venv/bin/activate` 21 | 22 | Install the dependencies: `pip install -r src/requirements.txt` 23 | 24 | Start the application with specific environment variables: `TRANSIENT_DB=true python src/main.py` 25 | 26 | ## Fork the collection and the environment in Postman 27 | Open Postman (desktop agent preferrably) 28 | 29 | Fork the collection and environment to your own workspace by clicking the **Run in Postman** button below. 30 | 31 | [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/17042069-561f3e8f-acc9-4909-8157-c69353630e95?action=collection%2Ffork&collection-url=entityId%3D17042069-561f3e8f-acc9-4909-8157-c69353630e95%26entityType%3Dcollection%26workspaceId%3De75d51a3-60cc-4e4c-9abd-92a87046399c#?env%5BThe%20Good%20Bank%20Environment%5D=W3sia2V5IjoiaG9zdCIsInZhbHVlIjoiaHR0cDovL3NlY3VyaXR5LnBvc3RtYW4tYnJlYWthYmxlLmNvbSIsImVuYWJsZWQiOnRydWV9XQ==) 32 | 33 | Or separately, fork the [collection](https://www.postman.com/postman/workspace/postman-live/collection/17042069-561f3e8f-acc9-4909-8157-c69353630e95) to a workspace of your choice. 34 | 35 | ![Forking the collection](/res/forking%20collection.png) 36 | 37 | And fork the [environment](https://www.postman.com/postman/workspace/postman-live/environment/17042069-1e57b415-ab9e-4028-bb73-b276d28458ac) to the same workspace where you forked the above collection. 38 | 39 | ![Forking the environment](/res/forking%20env.png) 40 | 41 | Set the initial value and current value of the `host` variable to `http://localhost:5000` 42 | 43 | Go back to the collections and start hacking! 44 | 45 | ## Using util (if using the docker setup) 46 | 47 | Login to the container 48 | 49 | `docker exec -it vuln-api-instance /bin/bash` 50 | 51 | Navigate to `/util` to use the JWT token break(or)make tool. 52 | 53 | `cd /util` 54 | 55 | ### Usage of JWT Token break(or)make 56 | 57 | ``` 58 | Usage: 59 | python3 brute_force_jwt_token.py make - to create a token using a leaked secret 60 | python3 brute_force_jwt_token.py break - to find the secret used by JWT token 61 | ``` 62 | 63 | __NOTE__: For non-containerized deployments, find the util directory in the repository itself. The required dependencies are already installed in the virtual environment. 64 | 65 | # Performance 66 | 67 | ## example 68 | ```bash 69 | $ ab -n 5000 -c 100 -T 'application/json' -p login.json http://127.0.0.1:5000/user/login 70 | ``` 71 | 72 | ``` 73 | This is ApacheBench, Version 2.3 <$Revision: 1879490 $> 74 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 75 | Licensed to The Apache Software Foundation, http://www.apache.org/ 76 | 77 | Benchmarking 127.0.0.1 (be patient) 78 | Completed 500 requests 79 | Completed 1000 requests 80 | Completed 1500 requests 81 | Completed 2000 requests 82 | Completed 2500 requests 83 | Completed 3000 requests 84 | Completed 3500 requests 85 | Completed 4000 requests 86 | Completed 4500 requests 87 | Completed 5000 requests 88 | Finished 5000 requests 89 | 90 | 91 | Server Software: Werkzeug/2.0.1 92 | Server Hostname: 127.0.0.1 93 | Server Port: 5000 94 | 95 | Document Path: /user/login 96 | Document Length: 68 bytes 97 | 98 | Concurrency Level: 100 99 | Time taken for tests: 31.257 seconds 100 | Complete requests: 5000 101 | Failed requests: 0 102 | Non-2xx responses: 5000 103 | Total transferred: 1100000 bytes 104 | Total body sent: 890000 105 | HTML transferred: 340000 bytes 106 | Requests per second: 159.96 [#/sec] (mean) 107 | Time per request: 625.137 [ms] (mean) 108 | Time per request: 6.251 [ms] (mean, across all concurrent requests) 109 | Transfer rate: 34.37 [Kbytes/sec] received 110 | 27.81 kb/s sent 111 | 62.17 kb/s total 112 | 113 | Connection Times (ms) 114 | min mean[+/-sd] median max 115 | Connect: 0 0 0.7 0 6 116 | Processing: 11 620 88.7 628 905 117 | Waiting: 6 614 88.0 623 893 118 | Total: 11 620 88.4 628 905 119 | 120 | Percentage of the requests served within a certain time (ms) 121 | 50% 628 122 | 66% 660 123 | 75% 677 124 | 80% 686 125 | 90% 711 126 | 95% 740 127 | 98% 838 128 | 99% 854 129 | 100% 905 (longest request) 130 | ``` 131 | 132 | -------------------------------------------------------------------------------- /openAPISpecBank.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: The Good Bank APIs 4 | version: 1.0.0 5 | servers: 6 | - url: http://security.postman-breakable.com 7 | tags: 8 | - name: Health 9 | - name: Bank User 10 | description: This folder contains APIs to manage users of The Good Bank. 11 | - name: Bank User > Authentication 12 | - name: Bank Account 13 | description: >- 14 | This folder contains the APIs that deals with user account and 15 | transactions. 16 | - name: Bank Admin 17 | description: >- 18 | This folder contains APIs used by bank administration. To provide support 19 | to customer in case of an issue. 20 | components: 21 | securitySchemes: 22 | ApiKeyAuth: 23 | type: apiKey 24 | in: header 25 | name: X-API-key 26 | paths: 27 | /: 28 | get: 29 | tags: 30 | - Health 31 | summary: Ping 32 | responses: 33 | '200': 34 | description: Successful response 35 | content: 36 | application/json: {} 37 | /user: 38 | put: 39 | tags: 40 | - Bank User > Authentication 41 | summary: Update User Information 42 | description: Authenticated users can update their user information. 43 | security: 44 | - ApiKeyAuth: [] 45 | requestBody: 46 | content: 47 | application/json: 48 | schema: 49 | type: object 50 | example: 51 | email_id: kmmanoj96@gulbank.com 52 | phone: +91 9876543210 53 | responses: 54 | '200': 55 | description: Successful response 56 | content: 57 | application/json: {} 58 | post: 59 | tags: 60 | - Bank User 61 | summary: Create User 62 | description: Register a new user with The Good Bank. The username must be unique. 63 | requestBody: 64 | content: 65 | application/json: 66 | schema: 67 | type: object 68 | example: 69 | username: kmmanoj96001 70 | password: kmmanoj96001pass 71 | responses: 72 | '200': 73 | description: Successful response 74 | content: 75 | application/json: {} 76 | /user/change-password: 77 | post: 78 | tags: 79 | - Bank User > Authentication 80 | summary: User Change Password 81 | description: >- 82 | This API is used to update the user password. The user must be 83 | authenticated and must remember the current password to update. 84 | requestBody: 85 | content: 86 | application/json: 87 | schema: 88 | type: object 89 | example: 90 | username: '' 91 | old_password: '' 92 | new_password: '' 93 | responses: 94 | '200': 95 | description: Successful response 96 | content: 97 | application/json: {} 98 | /user/logout: 99 | get: 100 | tags: 101 | - Bank User > Authentication 102 | summary: User Logout 103 | description: This API revokes an active user session. 104 | security: 105 | - ApiKeyAuth: [] 106 | responses: 107 | '200': 108 | description: Successful response 109 | content: 110 | application/json: {} 111 | /user/login: 112 | post: 113 | tags: 114 | - Bank User 115 | summary: User Login 116 | requestBody: 117 | content: 118 | application/json: 119 | schema: 120 | type: object 121 | example: 122 | username: kmmanoj96001 123 | password: kmmanoj96001pass 124 | responses: 125 | '200': 126 | description: Successful response 127 | content: 128 | application/json: {} 129 | /account/{user_id}/summary: 130 | get: 131 | tags: 132 | - Bank Account 133 | summary: Account summary 134 | description: Fetch account summary for a single user. 135 | parameters: 136 | - name: user_id 137 | in: path 138 | schema: 139 | type: string 140 | required: true 141 | responses: 142 | '200': 143 | description: Successful response 144 | content: 145 | application/json: {} 146 | /account/transactions: 147 | get: 148 | tags: 149 | - Bank Account 150 | summary: Account transactions 151 | description: Fetch transactions for a single account. 152 | security: 153 | - ApiKeyAuth: [] 154 | parameters: 155 | - name: limit 156 | in: query 157 | schema: 158 | type: integer 159 | description: number of transactions to display 160 | example: '10' 161 | - name: filter 162 | in: query 163 | schema: 164 | type: string 165 | description: Search string to filter on transaction party 166 | responses: 167 | '200': 168 | description: Successful response 169 | content: 170 | application/json: {} 171 | /people/customers: 172 | get: 173 | tags: 174 | - Bank Admin 175 | summary: Our customers 176 | security: 177 | - ApiKeyAuth: [] 178 | responses: 179 | '200': 180 | description: Successful response 181 | content: 182 | application/json: {} 183 | /admin/credit: 184 | post: 185 | tags: 186 | - Bank Admin 187 | summary: Credit amount 188 | description: >- 189 | To be used by the bank tellers to cash checks, receive deposits, savings 190 | account transactions among other things. 191 | security: 192 | - ApiKeyAuth: [] 193 | requestBody: 194 | content: 195 | application/json: 196 | schema: 197 | type: object 198 | example: 199 | user_id: user-id 200 | transaction_party: nowhere 201 | transaction_amount: 100 202 | responses: 203 | '200': 204 | description: Successful response 205 | content: 206 | application/json: {} 207 | /admin/debit: 208 | post: 209 | tags: 210 | - Bank Admin 211 | summary: Debit amount 212 | description: >- 213 | To be used by the bank tellers to cash checks, receive deposits, savings 214 | account transactions among other things. 215 | security: 216 | - ApiKeyAuth: [] 217 | requestBody: 218 | content: 219 | application/json: 220 | schema: 221 | type: object 222 | example: 223 | user_id: user-id 224 | transaction_party: somwhere 225 | transaction_amount: 100 226 | responses: 227 | '200': 228 | description: Successful response 229 | content: 230 | application/json: {} 231 | -------------------------------------------------------------------------------- /postman/The Good Bank Collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "561f3e8f-acc9-4909-8157-c69353630e95", 4 | "name": "The Good Bank APIs", 5 | "description": "This collection contains the APIs that power The Good Bank's client website and internal service portals.\n\nProceed with caution!", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Health", 11 | "item": [ 12 | { 13 | "name": "Ping", 14 | "request": { 15 | "method": "GET", 16 | "header": [], 17 | "url": { 18 | "raw": "{{host}}", 19 | "host": [ 20 | "{{host}}" 21 | ] 22 | } 23 | }, 24 | "response": [] 25 | } 26 | ] 27 | }, 28 | { 29 | "name": "Bank User", 30 | "item": [ 31 | { 32 | "name": "Authentication", 33 | "item": [ 34 | { 35 | "name": "Update User Information", 36 | "request": { 37 | "method": "PUT", 38 | "header": [], 39 | "body": { 40 | "mode": "raw", 41 | "raw": "{\n \"email_id\": \"kmmanoj96@gulbank.com\",\n \"phone\": \"+91 9876543210\"\n}", 42 | "options": { 43 | "raw": { 44 | "language": "json" 45 | } 46 | } 47 | }, 48 | "url": { 49 | "raw": "{{host}}/user", 50 | "host": [ 51 | "{{host}}" 52 | ], 53 | "path": [ 54 | "user" 55 | ] 56 | }, 57 | "description": "Authenticated users can update their user information." 58 | }, 59 | "response": [] 60 | }, 61 | { 62 | "name": "Get User Information", 63 | "request": { 64 | "method": "GET", 65 | "header": [], 66 | "url": { 67 | "raw": "{{host}}/user", 68 | "host": [ 69 | "{{host}}" 70 | ], 71 | "path": [ 72 | "user" 73 | ] 74 | }, 75 | "description": "Authenticated users can fetch their user information." 76 | }, 77 | "response": [] 78 | }, 79 | { 80 | "name": "User Change Password", 81 | "request": { 82 | "method": "POST", 83 | "header": [], 84 | "body": { 85 | "mode": "raw", 86 | "raw": "{\n \"username\": \"\",\n \"old_password\": \"\",\n \"new_password\": \"\"\n}", 87 | "options": { 88 | "raw": { 89 | "language": "json" 90 | } 91 | } 92 | }, 93 | "url": { 94 | "raw": "{{host}}/user/change-password", 95 | "host": [ 96 | "{{host}}" 97 | ], 98 | "path": [ 99 | "user", 100 | "change-password" 101 | ] 102 | }, 103 | "description": "This API is used to update the user password. The user must be authenticated and must remember the current password to update." 104 | }, 105 | "response": [] 106 | }, 107 | { 108 | "name": "User Logout", 109 | "request": { 110 | "method": "GET", 111 | "header": [], 112 | "url": { 113 | "raw": "{{host}}/user/logout", 114 | "host": [ 115 | "{{host}}" 116 | ], 117 | "path": [ 118 | "user", 119 | "logout" 120 | ] 121 | }, 122 | "description": "This API revokes an active user session." 123 | }, 124 | "response": [] 125 | } 126 | ], 127 | "auth": { 128 | "type": "apikey", 129 | "apikey": [ 130 | { 131 | "key": "value", 132 | "value": "{{session_token}}", 133 | "type": "string" 134 | }, 135 | { 136 | "key": "key", 137 | "value": "X-API-key", 138 | "type": "string" 139 | } 140 | ] 141 | }, 142 | "event": [ 143 | { 144 | "listen": "prerequest", 145 | "script": { 146 | "type": "text/javascript", 147 | "exec": [ 148 | "" 149 | ] 150 | } 151 | }, 152 | { 153 | "listen": "test", 154 | "script": { 155 | "type": "text/javascript", 156 | "exec": [ 157 | "" 158 | ] 159 | } 160 | } 161 | ] 162 | }, 163 | { 164 | "name": "Create User", 165 | "request": { 166 | "method": "POST", 167 | "header": [], 168 | "body": { 169 | "mode": "raw", 170 | "raw": "{\n \"username\": \"kmmanoj96001\",\n \"password\": \"kmmanoj96001pass\"\n}", 171 | "options": { 172 | "raw": { 173 | "language": "json" 174 | } 175 | } 176 | }, 177 | "url": { 178 | "raw": "{{host}}/user", 179 | "host": [ 180 | "{{host}}" 181 | ], 182 | "path": [ 183 | "user" 184 | ] 185 | }, 186 | "description": "Register a new user with The Good Bank. The username must be unique." 187 | }, 188 | "response": [] 189 | }, 190 | { 191 | "name": "User Login", 192 | "event": [ 193 | { 194 | "listen": "test", 195 | "script": { 196 | "exec": [ 197 | "pm.test(\"Status code is 200\", function () {", 198 | " pm.response.to.have.status(200);", 199 | " ", 200 | " var jsonData = pm.response.json();", 201 | " ", 202 | " let session_token = jsonData.response.session_token;", 203 | " pm.environment.set(\"session_token\", session_token);", 204 | "", 205 | " let user_id = jsonData.response.user_id;", 206 | " pm.environment.set(\"user_id\", user_id);", 207 | "});" 208 | ], 209 | "type": "text/javascript" 210 | } 211 | } 212 | ], 213 | "request": { 214 | "method": "POST", 215 | "header": [], 216 | "body": { 217 | "mode": "raw", 218 | "raw": "{\n \"username\": \"kmmanoj96001\",\n \"password\": \"kmmanoj96001pass\"\n}", 219 | "options": { 220 | "raw": { 221 | "language": "json" 222 | } 223 | } 224 | }, 225 | "url": { 226 | "raw": "{{host}}/user/login", 227 | "host": [ 228 | "{{host}}" 229 | ], 230 | "path": [ 231 | "user", 232 | "login" 233 | ] 234 | } 235 | }, 236 | "response": [] 237 | } 238 | ], 239 | "description": "This folder contains APIs to manage users of The Good Bank." 240 | }, 241 | { 242 | "name": "Bank Account", 243 | "item": [ 244 | { 245 | "name": "Account summary", 246 | "request": { 247 | "method": "GET", 248 | "header": [], 249 | "url": { 250 | "raw": "{{host}}/account/{{user_id}}/summary", 251 | "host": [ 252 | "{{host}}" 253 | ], 254 | "path": [ 255 | "account", 256 | "{{user_id}}", 257 | "summary" 258 | ] 259 | }, 260 | "description": "Fetch account summary for a single user." 261 | }, 262 | "response": [] 263 | }, 264 | { 265 | "name": "Account transactions", 266 | "request": { 267 | "method": "GET", 268 | "header": [], 269 | "url": { 270 | "raw": "{{host}}/account/transactions?limit=10&filter=", 271 | "host": [ 272 | "{{host}}" 273 | ], 274 | "path": [ 275 | "account", 276 | "transactions" 277 | ], 278 | "query": [ 279 | { 280 | "key": "limit", 281 | "value": "10", 282 | "description": "number of transactions to display" 283 | }, 284 | { 285 | "key": "filter", 286 | "value": "", 287 | "description": "Search string to filter on transaction party" 288 | } 289 | ] 290 | }, 291 | "description": "Fetch transactions for a single account." 292 | }, 293 | "response": [] 294 | } 295 | ], 296 | "description": "This folder contains the APIs that deals with user account and transactions.", 297 | "auth": { 298 | "type": "apikey", 299 | "apikey": [ 300 | { 301 | "key": "value", 302 | "value": "{{session_token}}", 303 | "type": "string" 304 | }, 305 | { 306 | "key": "key", 307 | "value": "X-API-key", 308 | "type": "string" 309 | } 310 | ] 311 | }, 312 | "event": [ 313 | { 314 | "listen": "prerequest", 315 | "script": { 316 | "type": "text/javascript", 317 | "exec": [ 318 | "" 319 | ] 320 | } 321 | }, 322 | { 323 | "listen": "test", 324 | "script": { 325 | "type": "text/javascript", 326 | "exec": [ 327 | "" 328 | ] 329 | } 330 | } 331 | ] 332 | }, 333 | { 334 | "name": "Bank Admin", 335 | "item": [ 336 | { 337 | "name": "Our customers", 338 | "request": { 339 | "method": "GET", 340 | "header": [], 341 | "url": { 342 | "raw": "{{host}}/people/customers", 343 | "host": [ 344 | "{{host}}" 345 | ], 346 | "path": [ 347 | "people", 348 | "customers" 349 | ] 350 | } 351 | }, 352 | "response": [] 353 | }, 354 | { 355 | "name": "Credit amount", 356 | "request": { 357 | "method": "POST", 358 | "header": [], 359 | "body": { 360 | "mode": "raw", 361 | "raw": "{\n \"user_id\": \"user-id\",\n \"transaction_party\": \"nowhere\",\n \"transaction_amount\": 100\n}", 362 | "options": { 363 | "raw": { 364 | "language": "json" 365 | } 366 | } 367 | }, 368 | "url": { 369 | "raw": "{{host}}/admin/credit", 370 | "host": [ 371 | "{{host}}" 372 | ], 373 | "path": [ 374 | "admin", 375 | "credit" 376 | ] 377 | }, 378 | "description": "To be used by the bank tellers to cash checks, receive deposits, savings account transactions among other things." 379 | }, 380 | "response": [] 381 | }, 382 | { 383 | "name": "Debit amount", 384 | "request": { 385 | "method": "POST", 386 | "header": [], 387 | "body": { 388 | "mode": "raw", 389 | "raw": "{\n \"user_id\": \"user-id\",\n \"transaction_party\": \"somwhere\",\n \"transaction_amount\": 100\n}", 390 | "options": { 391 | "raw": { 392 | "language": "json" 393 | } 394 | } 395 | }, 396 | "url": { 397 | "raw": "{{host}}/admin/debit", 398 | "host": [ 399 | "{{host}}" 400 | ], 401 | "path": [ 402 | "admin", 403 | "debit" 404 | ] 405 | }, 406 | "description": "To be used by the bank tellers to cash checks, receive deposits, savings account transactions among other things." 407 | }, 408 | "response": [] 409 | } 410 | ], 411 | "description": "This folder contains APIs used by bank administration. To provide support to customer in case of an issue.", 412 | "auth": { 413 | "type": "apikey", 414 | "apikey": [ 415 | { 416 | "key": "value", 417 | "value": "{{session_token}}", 418 | "type": "string" 419 | }, 420 | { 421 | "key": "key", 422 | "value": "X-API-key", 423 | "type": "string" 424 | } 425 | ] 426 | }, 427 | "event": [ 428 | { 429 | "listen": "prerequest", 430 | "script": { 431 | "type": "text/javascript", 432 | "exec": [ 433 | "" 434 | ] 435 | } 436 | }, 437 | { 438 | "listen": "test", 439 | "script": { 440 | "type": "text/javascript", 441 | "exec": [ 442 | "" 443 | ] 444 | } 445 | } 446 | ] 447 | } 448 | ] 449 | } -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import os 4 | 5 | from uuid import uuid4 6 | from flask import Flask, request 7 | from core.logger import logging 8 | from core.db import DB 9 | from core.constants import USER_TABLE 10 | from jwt import encode as jwt_encode, decode as jwt_decode 11 | 12 | app = Flask(__name__) 13 | jwt_secret = 'bankbank' 14 | SERVER_ERROR = 'Oops! Something is wrong at our end! Please try again later.' 15 | PASS_CACHE = dict() 16 | 17 | if os.getenv('TRANSIENT_DB') is not None: 18 | logging.info('Recreating DB') 19 | if os.path.exists('db.sqlite3'): 20 | os.remove('db.sqlite3') 21 | DB.initialize_db() 22 | 23 | @app.route('/', methods=['GET']) 24 | def welcome(): 25 | return dict( 26 | application='gullible-bank', 27 | version='1.0.0', 28 | status='running' 29 | ) 30 | 31 | @app.route('/user', methods=['POST']) 32 | def create_user(): 33 | data = request.json 34 | if not data or 'username' not in data or 'password' not in data: 35 | return dict(status='FORBIDDEN', message='missing credentials'), 403 36 | username = data['username'] 37 | password = data['password'] 38 | pattern = r'^[0-9A-Za-z]+$' 39 | if not re.match(pattern, username) or not re.match(pattern, password): 40 | logging.error(f'Bad pattern') 41 | return dict(status='FORBIDDEN', message=f'username and password should match the pattern {pattern}'), 403 42 | 43 | try: 44 | users_query = f"SELECT * FROM {USER_TABLE} WHERE username = '{username}'" 45 | users = DB.retrieve(users_query) 46 | if len(users) > 0: 47 | logging.error(f'No user having username={username}') 48 | return dict(status='FORBIDDEN', message=f'Username already taken!'), 403 49 | user_id = uuid4() 50 | insert_user = f"INSERT INTO {USER_TABLE} values('{user_id}', '{username}', '', '', '{password}', 0, 0, '')" 51 | DB.modify(insert_user) 52 | insert_acc_sum = f"INSERT INTO account_summary values('{user_id}', 0, 0)" 53 | DB.modify(insert_acc_sum) 54 | user = dict() 55 | user['user_id'] = user_id 56 | user['username'] = username 57 | return dict(status='OK', response=user), 200 58 | except Exception as e: 59 | logging.error(str(e)) 60 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 61 | 62 | # API2:2019 Broken Authentication 63 | def get_info_from_token(api_token): 64 | if not api_token: 65 | return None 66 | try: 67 | decoded = jwt_decode(api_token, jwt_secret, algorithms=["HS256"]) 68 | if decoded['expires'] < time.time(): 69 | logging.warning("Token expired") 70 | return None 71 | for key, value in decoded.items(): 72 | if not re.match(r'^[0-9A-Za-z_]+$', key) or not re.match(r'^[0-9A-Za-z-.]+$', str(value)): 73 | logging.error('Bad pattern in token') 74 | raise Exception('Bad pattern') 75 | session_query = f"SELECT * FROM {USER_TABLE} WHERE session_token = '{api_token}'" 76 | records = DB.retrieve(session_query) 77 | return decoded if len(records) > 0 else None 78 | except Exception as e: 79 | logging.error(str(e)) 80 | return None 81 | 82 | @app.route('/user', methods=['GET']) 83 | def get_user_info(): 84 | session_token = request.headers.get('X-API-key') 85 | user_info = get_info_from_token(session_token) 86 | if not user_info: 87 | logging.warning(f'No User info') 88 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 89 | user_record_query = f"SELECT * FROM {USER_TABLE} WHERE user_id = '{user_info['user_id']}'" 90 | user = DB.retrieve(user_record_query)[0] 91 | userd = dict() 92 | userd['user_id'], userd['username'], userd['email_id'], userd['phone_number'], _, userd['employee'], userd['failed_logins'], _ = user 93 | return dict(status='OK', response=userd) 94 | 95 | # API6:2019 Mass assignment 96 | @app.route('/user', methods=['PUT']) 97 | def update_user_info(): 98 | session_token = request.headers.get('X-API-key') 99 | user_info = get_info_from_token(session_token) 100 | if not user_info: 101 | logging.warning(f'No User info') 102 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 103 | updatables = request.json 104 | if not updatables: 105 | logging.warning(f'No updatable User info') 106 | return dict(status='FORBIDDEN', message=f'Missing information'), 403 107 | updates = [] 108 | for key, value in updatables.items(): 109 | if not re.match(r'^[0-9A-Za-z@+. ]+$', str(value)): continue 110 | if type(value) in [int, float]: 111 | updates.append(f"{key} = {value}") 112 | else: 113 | updates.append(f"{key} = '{value}'") 114 | if len(updates) > 0: 115 | try: 116 | update_query = f"UPDATE {USER_TABLE} SET {', '.join(updates)} WHERE user_id = '{user_info['user_id']}'" 117 | DB.modify(update_query) 118 | except Exception as e: 119 | logging.error(str(e)) 120 | user_record_query = f"SELECT * FROM {USER_TABLE} WHERE user_id = '{user_info['user_id']}'" 121 | user = DB.retrieve(user_record_query)[0] 122 | userd = dict() 123 | userd['user_id'], userd['username'], userd['email_id'], userd['phone_number'], _, userd['employee'], userd['failed_logins'], _ = user 124 | return dict(status='OK', response=userd) 125 | 126 | @app.route('/user/login', methods=['POST']) 127 | def login(): 128 | data = request.json 129 | if not data or 'username' not in data or 'password' not in data: 130 | return dict(status='FORBIDDEN', message='missing credentials'), 403 131 | username = data['username'] 132 | password = data['password'] 133 | pattern = r'^[0-9A-Za-z]+$' 134 | if not re.match(pattern, username) or not re.match(pattern, password): 135 | logging.error(f'Bad pattern') 136 | return dict(status='FORBIDDEN', message=f'Authentication Failed!'), 403 137 | 138 | try: 139 | users_query = f"SELECT * FROM {USER_TABLE} WHERE username = '{username}'" 140 | users = DB.retrieve(users_query) 141 | if len(users) == 0: 142 | return dict(status='FORBIDDEN', message=f'Authentication Failed!'), 403 143 | user = users[0] 144 | if user[6] + 1 > 3: 145 | return dict(status='FORBIDDEN', message=f'User locked out! Try again later'), 403 146 | if user[4] != password: 147 | update_loginfail = f"UPDATE {USER_TABLE} SET failed_logins = {user[6]+1} WHERE username = '{username}'" 148 | DB.modify(update_loginfail) 149 | return dict(status='FORBIDDEN', message=f'Authentication Failed!'), 403 150 | 151 | userd = dict() 152 | userd['user_id'] = user[0] 153 | userd['username'] = user[1] 154 | userd['expires'] = (time.time() + 3600) 155 | if int(user[5]) == 1: 156 | logging.info(f'Is an employee') 157 | userd['aRe_yOu_An_EmPlOyEe'] = True 158 | else: 159 | logging.info(f'Is NOT an employee') 160 | userd['session_token'] = jwt_encode(userd, jwt_secret, algorithm='HS256') 161 | 162 | update_session = f"UPDATE {USER_TABLE} SET failed_logins = 0, session_token = '{userd['session_token']}' WHERE username = '{username}'" 163 | DB.modify(update_session) 164 | return dict(status='OK', response=userd), 200 165 | except Exception as e: 166 | logging.error(e) 167 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 168 | 169 | # API4:2019 Lack of resources and rate limiting 170 | @app.route('/user/change-password', methods=['POST']) 171 | def change_password(): 172 | global PASS_CACHE 173 | session_token = request.headers.get('X-API-key') 174 | user_info = get_info_from_token(session_token) 175 | if not user_info: 176 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 177 | 178 | data = request.json 179 | if not data or 'username' not in data or 'old_password' not in data or 'new_password' not in data: 180 | return dict(status='FORBIDDEN', message='missing credentials'), 403 181 | username = data['username'] 182 | old_pass = data['old_password'] 183 | new_pass = data['new_password'] 184 | pattern = r'^[0-9A-Za-z]+$' 185 | if not re.match(pattern, username) or not re.match(pattern, old_pass) or not re.match(pattern, new_pass): 186 | logging.info(f'Bad pattern') 187 | return dict(status='FORBIDDEN', message=f'username and passwords should match the pattern {pattern}'), 403 188 | 189 | old_pass_val = None 190 | logging.debug(username + str(PASS_CACHE)) 191 | if username in PASS_CACHE: 192 | logging.info(f'using cache') 193 | old_pass_val = PASS_CACHE[username] 194 | else: 195 | logging.info(f'NOT using cache') 196 | pass_query = f"SELECT password FROM {USER_TABLE} WHERE username = '{username}'" 197 | old_pass_val_record = DB.retrieve(pass_query) 198 | if len(old_pass_val_record) == 0: 199 | return dict(status='FORBIDDEN', message=f'Invalid username'), 403 200 | old_pass_val = old_pass_val_record[0][0] 201 | PASS_CACHE[username] = old_pass_val 202 | 203 | logging.debug(PASS_CACHE) 204 | 205 | if old_pass != old_pass_val: 206 | return dict(status='FORBIDDEN', message=f'Wrong old password'), 403 207 | 208 | try: 209 | update_pass = f"UPDATE {USER_TABLE} SET password = '{new_pass}' WHERE username = '{username}'" 210 | DB.modify(update_pass) 211 | PASS_CACHE[username] = new_pass 212 | logging.debug(PASS_CACHE) 213 | update_loginfail = f"UPDATE {USER_TABLE} SET failed_logins = 0 WHERE username = '{username}'" 214 | DB.modify(update_loginfail) 215 | return dict(status='OK', message=f'Password updated successfully!'), 200 216 | except Exception as e: 217 | logging.error(str(e)) 218 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 219 | 220 | @app.route('/user/logout', methods=['GET']) 221 | def logout(): 222 | session_token = request.headers.get('X-API-key') 223 | user_info = get_info_from_token(session_token) 224 | if not user_info: 225 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 226 | try: 227 | update_session = f"UPDATE {USER_TABLE} SET session_token = '' WHERE user_id = '{user_info['user_id']}'" 228 | DB.modify(update_session) 229 | return dict(status='OK', response='Successfully logged out!'), 200 230 | except Exception as e: 231 | logging.error(str(e)) 232 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 233 | 234 | # API1:2019 Broken object level authorization 235 | @app.route('/account//summary', methods=['GET']) 236 | def get_account_summary(user_id): 237 | session_token = request.headers.get('X-API-key') 238 | user_info = get_info_from_token(session_token) 239 | if not user_info: 240 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 241 | if not re.match(r'^[0-9A-Za-z-]+$', user_id): 242 | return dict(status='FORBIDDEN', message=f'Bad pattern User ID!'), 403 243 | try: 244 | summary_statement = f"SELECT * FROM account_summary WHERE user_id = '{user_id}'" 245 | account_summary = DB.retrieve(summary_statement) 246 | response = dict() 247 | response['user_id'], response['balance'], response['last_transaction'] = account_summary[0] 248 | return dict(status='OK', response=response), 200 249 | except Exception as e: 250 | logging.error(str(e)) 251 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 252 | 253 | # API7:2019 Security Misconfiguration 254 | # API8:2019 Injection 255 | @app.route('/account/transactions', methods=['GET']) 256 | def get_account_transactions(): 257 | session_token = request.headers.get('X-API-key') 258 | user_info = get_info_from_token(session_token) 259 | if not user_info: 260 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 261 | try: 262 | limit = request.args.get('limit') 263 | limit = int(limit) if limit else 10 264 | 265 | search_string = request.args.get('filter') 266 | search_string = search_string if search_string else '' 267 | transactions_statement = f"SELECT * FROM account_transactions WHERE user_id = '{user_info['user_id']}' AND transaction_party LIKE '%{search_string}%' ORDER BY transaction_time DESC LIMIT {limit};" 268 | transactions = DB.retrieve(transactions_statement) 269 | response = list() 270 | for transaction in transactions: 271 | t = dict() 272 | t['user_id'], t['transaction_time'], t['transaction_party'], t['transaction_type'], t['transaction_amount'], t['balance'] = transaction 273 | response.append(t) 274 | return dict(status='OK', response=response), 200 275 | except Exception as e: 276 | logging.error(str(e)) 277 | # TODO: CORS allow all headers 278 | return dict(status='SERVER ERROR', response=str(e)), 500 279 | 280 | # API3:2019 Excessive data exposure 281 | @app.route('/people/customers', methods=['GET']) 282 | def get_customers(): 283 | try: 284 | customers_query = f"SELECT * FROM {USER_TABLE} WHERE employee = 0" 285 | customers = DB.retrieve(customers_query) 286 | result = list() 287 | for customer in customers: 288 | cd = dict() 289 | cd['user_id'], cd['username'], cd['email_id'], cd['phone'] = customer[:4] 290 | result.append(cd) 291 | return dict(status='OK', response=result), 200 292 | except Exception as e: 293 | logging.error(str(e)) 294 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 295 | 296 | # HIDDEN FROM COLLECTIONS 297 | # API5:2019 Broken function level authorization 298 | @app.route('/people/employees', methods=['GET']) 299 | def get_admins(): 300 | try: 301 | employees_query = f"SELECT * FROM {USER_TABLE} WHERE employee = 1" 302 | employees = DB.retrieve(employees_query) 303 | result = list() 304 | for employee in employees: 305 | ed = dict() 306 | ed['user_id'], ed['username'], ed['email_id'], ed['phone'] = employee[:4] 307 | result.append(ed) 308 | return dict(status='OK', response=result), 200 309 | except Exception as e: 310 | logging.error(str(e)) 311 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 312 | 313 | @app.route('/admin/credit', methods=['POST']) 314 | def admin_credit(): 315 | session_token = request.headers.get('X-API-key') 316 | user_info = get_info_from_token(session_token) 317 | if not user_info: 318 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 319 | 320 | if 'aRe_yOu_An_EmPlOyEe' not in user_info or not (user_info['aRe_yOu_An_EmPlOyEe'] == True): 321 | return dict(status='FORBIDDEN', message='You are not an admin!'), 403 322 | 323 | data = request.json 324 | if not data or 'user_id' not in data or 'transaction_party' not in data or 'transaction_amount' not in data: 325 | return dict(status='FORBIDDEN', message='missing information'), 403 326 | try: 327 | user_id = data['user_id'] 328 | tr_time = time.time() 329 | tr_party = data['transaction_party'] 330 | tr_amount = max(0, float(data['transaction_amount'])) 331 | 332 | pattern = r'^[0-9A-Za-z- ]+$' 333 | if not re.match(pattern, user_id) or not re.match(pattern, tr_party): 334 | return dict(status='FORBIDDEN', message=f'Bad pattern for user ID or transaction party. Should match {pattern}'), 403 335 | 336 | get_balance_query = f"SELECT balance FROM account_summary WHERE user_id = '{user_id}'" 337 | balance_record = DB.retrieve(get_balance_query) 338 | if len(balance_record) == 0: 339 | return dict(status='FORBIDDEN', message=f'Invalid User ID!'), 403 340 | balance = balance_record[0][0] 341 | balance += tr_amount 342 | 343 | add_tr_query = f"INSERT INTO account_transactions values('{user_id}', {tr_time}, '{tr_party}', 'credit', {tr_amount}, {balance})" 344 | DB.modify(add_tr_query) 345 | 346 | update_balance = f"UPDATE account_summary SET balance = {balance}, last_transaction = {tr_time} WHERE user_id = '{user_id}'" 347 | DB.modify(update_balance) 348 | 349 | return dict(status='OK', response='Amount credited'), 200 350 | 351 | except Exception as e: 352 | logging.error(str(e)) 353 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 354 | 355 | @app.route('/admin/debit', methods=['POST']) 356 | def admin_debit(): 357 | session_token = request.headers.get('X-API-key') 358 | user_info = get_info_from_token(session_token) 359 | if not user_info: 360 | return dict(status='FORBIDDEN', message=f'Unauthenticated!'), 403 361 | 362 | if 'aRe_yOu_An_EmPlOyEe' not in user_info or not (user_info['aRe_yOu_An_EmPlOyEe'] == True): 363 | return dict(status='FORBIDDEN', message='You are not an admin!'), 403 364 | 365 | data = request.json 366 | if not data or 'user_id' not in data or 'transaction_party' not in data or 'transaction_amount' not in data: 367 | return dict(status='FORBIDDEN', message='missing information'), 403 368 | 369 | try: 370 | user_id = data['user_id'] 371 | tr_time = time.time() 372 | tr_party = data['transaction_party'] 373 | tr_amount = max(0, float(data['transaction_amount'])) 374 | 375 | pattern = r'^[0-9A-Za-z- ]+$' 376 | if not re.match(pattern, user_id) or not re.match(pattern, tr_party): 377 | return dict(status='FORBIDDEN', message=f'Bad pattern for user ID or transaction party. Should match {pattern}'), 403 378 | 379 | get_balance_query = f"SELECT balance FROM account_summary WHERE user_id = '{user_id}'" 380 | balance_record = DB.retrieve(get_balance_query) 381 | if len(balance_record) == 0: 382 | return dict(status='FORBIDDEN', message=f'Invalid User ID!'), 403 383 | balance = balance_record[0][0] 384 | if balance < tr_amount: 385 | return dict(status='FORBIDDEN', message=f'Insufficient Balance!'), 403 386 | balance -= tr_amount 387 | 388 | add_tr_query = f"INSERT INTO account_transactions values('{user_id}', {tr_time}, '{tr_party}', 'debit', {tr_amount}, {balance})" 389 | DB.modify(add_tr_query) 390 | 391 | update_balance = f"UPDATE account_summary SET balance = {balance}, last_transaction = {tr_time} WHERE user_id = '{user_id}'" 392 | DB.modify(update_balance) 393 | 394 | return dict(status='OK', response='Amount debited'), 200 395 | 396 | except Exception as e: 397 | logging.error(str(e)) 398 | return dict(status='SERVER ERROR', response=SERVER_ERROR), 500 399 | 400 | 401 | app.run(debug=True, host='0.0.0.0') --------------------------------------------------------------------------------