├── .gitignore ├── Dockerfile ├── LICENSE ├── Postman.json ├── README.md ├── config.json ├── config.py ├── guide └── README.md ├── install_db.py ├── requirements.txt ├── restgoat.db ├── routes.py └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | Pipfile 2 | Pipfile.lock 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | 7 | RUN pip install --no-cache-dir -r requirements.txt 8 | 9 | COPY . . 10 | 11 | ENV FLASK_APP=server.py 12 | 13 | ENTRYPOINT [ "flask" ] 14 | 15 | CMD ["run", "--host", "0.0.0.0"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Steven Hartz, Optiv Security Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 4 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 11 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 12 | IN THE SOFTWARE. 13 | -------------------------------------------------------------------------------- /Postman.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "10af27c6-4f86-4f76-adbd-71484e2fec26", 4 | "name": "REST API Goat", 5 | "description": "Test cases for the REST API Goat", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Authenticate", 11 | "request": { 12 | "method": "POST", 13 | "header": [ 14 | { 15 | "key": "Content-Type", 16 | "value": "application/json", 17 | "type": "text" 18 | } 19 | ], 20 | "body": { 21 | "mode": "raw", 22 | "raw": "{\n\t\"api_token\": \"vfuzd2nvaweojqolm4kq\"\n}", 23 | "options": { 24 | "raw": { 25 | "language": "json" 26 | } 27 | } 28 | }, 29 | "url": { 30 | "raw": "http://localhost:5000/authenticate", 31 | "protocol": "http", 32 | "host": [ 33 | "localhost" 34 | ], 35 | "port": "5000", 36 | "path": [ 37 | "authenticate" 38 | ] 39 | } 40 | }, 41 | "response": [] 42 | }, 43 | { 44 | "name": "Get Customers", 45 | "request": { 46 | "method": "GET", 47 | "header": [ 48 | { 49 | "key": "X-API-Token", 50 | "value": "vfuzd2nvaweojqolm4kq", 51 | "type": "text" 52 | }, 53 | { 54 | "key": "Content-Type", 55 | "value": "application/json", 56 | "type": "text" 57 | } 58 | ], 59 | "url": { 60 | "raw": "http://localhost:5000/get_customers", 61 | "protocol": "http", 62 | "host": [ 63 | "localhost" 64 | ], 65 | "port": "5000", 66 | "path": [ 67 | "get_customers" 68 | ] 69 | } 70 | }, 71 | "response": [] 72 | }, 73 | { 74 | "name": "Get Customer v1 (ID)", 75 | "request": { 76 | "method": "GET", 77 | "header": [ 78 | { 79 | "key": "X-API-Token", 80 | "value": "vfuzd2nvaweojqolm4kq", 81 | "type": "text" 82 | }, 83 | { 84 | "key": "Content-Type", 85 | "value": "application/json", 86 | "type": "text" 87 | } 88 | ], 89 | "url": { 90 | "raw": "http://localhost:5000/get_customer_v1/2", 91 | "protocol": "http", 92 | "host": [ 93 | "localhost" 94 | ], 95 | "port": "5000", 96 | "path": [ 97 | "get_customer_v1", 98 | "2" 99 | ] 100 | } 101 | }, 102 | "response": [] 103 | }, 104 | { 105 | "name": "Get Customer v2 (ID)", 106 | "request": { 107 | "method": "GET", 108 | "header": [ 109 | { 110 | "key": "X-API-Token", 111 | "value": "vfuzd2nvaweojqolm4kq", 112 | "type": "text" 113 | }, 114 | { 115 | "key": "Content-Type", 116 | "value": "application/json", 117 | "type": "text" 118 | } 119 | ], 120 | "url": { 121 | "raw": "http://localhost:5000/get_customer_v2/2", 122 | "protocol": "http", 123 | "host": [ 124 | "localhost" 125 | ], 126 | "port": "5000", 127 | "path": [ 128 | "get_customer_v2", 129 | "2" 130 | ] 131 | } 132 | }, 133 | "response": [] 134 | }, 135 | { 136 | "name": "Create Transfer", 137 | "request": { 138 | "method": "PUT", 139 | "header": [ 140 | { 141 | "key": "X-API-Token", 142 | "value": "vfuzd2nvaweojqolm4kq", 143 | "type": "text" 144 | }, 145 | { 146 | "key": "Content-Type", 147 | "value": "application/json", 148 | "type": "text" 149 | } 150 | ], 151 | "body": { 152 | "mode": "raw", 153 | "raw": "{\n\t\"from\": 5,\n\t\"to\": 3,\n\t\"ammount\": \"500\"\n}", 154 | "options": { 155 | "raw": { 156 | "language": "json" 157 | } 158 | } 159 | }, 160 | "url": { 161 | "raw": "http://localhost:5000/transfer", 162 | "protocol": "http", 163 | "host": [ 164 | "localhost" 165 | ], 166 | "port": "5000", 167 | "path": [ 168 | "transfer" 169 | ] 170 | } 171 | }, 172 | "response": [] 173 | }, 174 | { 175 | "name": "Get Transfers (All)", 176 | "request": { 177 | "method": "GET", 178 | "header": [ 179 | { 180 | "key": "X-API-Token", 181 | "value": "vfuzd2nvaweojqolm4kq", 182 | "type": "text" 183 | }, 184 | { 185 | "key": "Content-Type", 186 | "value": "application/json", 187 | "type": "text" 188 | } 189 | ], 190 | "url": { 191 | "raw": "http://localhost:5000/get_transfers", 192 | "protocol": "http", 193 | "host": [ 194 | "localhost" 195 | ], 196 | "port": "5000", 197 | "path": [ 198 | "get_transfers" 199 | ] 200 | } 201 | }, 202 | "response": [] 203 | }, 204 | { 205 | "name": "Get Transfers (Created)", 206 | "request": { 207 | "method": "GET", 208 | "header": [ 209 | { 210 | "key": "X-API-Token", 211 | "value": "vfuzd2nvaweojqolm4kq", 212 | "type": "text" 213 | }, 214 | { 215 | "key": "Content-Type", 216 | "value": "application/json", 217 | "type": "text" 218 | } 219 | ], 220 | "url": { 221 | "raw": "http://localhost:5000/get_transfers/CREATED", 222 | "protocol": "http", 223 | "host": [ 224 | "localhost" 225 | ], 226 | "port": "5000", 227 | "path": [ 228 | "get_transfers", 229 | "CREATED" 230 | ] 231 | } 232 | }, 233 | "response": [] 234 | }, 235 | { 236 | "name": "Process Transfers", 237 | "request": { 238 | "method": "POST", 239 | "header": [ 240 | { 241 | "key": "X-API-Token", 242 | "value": "vfuzd2nvaweojqolm4kq", 243 | "type": "text" 244 | }, 245 | { 246 | "key": "Content-Type", 247 | "value": "application/json", 248 | "type": "text" 249 | } 250 | ], 251 | "url": { 252 | "raw": "http://localhost:5000/process_transfers", 253 | "protocol": "http", 254 | "host": [ 255 | "localhost" 256 | ], 257 | "port": "5000", 258 | "path": [ 259 | "process_transfers" 260 | ] 261 | } 262 | }, 263 | "response": [] 264 | }, 265 | { 266 | "name": "Confirm Transfer", 267 | "request": { 268 | "method": "POST", 269 | "header": [ 270 | { 271 | "key": "X-API-Token", 272 | "value": "vfuzd2nvaweojqolm4kq", 273 | "type": "text" 274 | }, 275 | { 276 | "key": "Content-Type", 277 | "value": "application/json", 278 | "type": "text" 279 | } 280 | ], 281 | "url": { 282 | "raw": "http://localhost:5000/confirm_transfer/1", 283 | "protocol": "http", 284 | "host": [ 285 | "localhost" 286 | ], 287 | "port": "5000", 288 | "path": [ 289 | "confirm_transfer", 290 | "1" 291 | ] 292 | } 293 | }, 294 | "response": [] 295 | }, 296 | { 297 | "name": "Create Access Token", 298 | "request": { 299 | "method": "POST", 300 | "header": [ 301 | { 302 | "key": "X-API-Token", 303 | "value": "vfuzd2nvaweojqolm4kq", 304 | "type": "text" 305 | }, 306 | { 307 | "key": "Content-Type", 308 | "value": "application/json", 309 | "type": "text" 310 | } 311 | ], 312 | "url": { 313 | "raw": "http://localhost:5000/new_token", 314 | "protocol": "http", 315 | "host": [ 316 | "localhost" 317 | ], 318 | "port": "5000", 319 | "path": [ 320 | "new_token" 321 | ] 322 | }, 323 | "description": "This is used by banks when they want to add a new API token for a client to use." 324 | }, 325 | "response": [] 326 | }, 327 | { 328 | "name": "Delete Token", 329 | "request": { 330 | "method": "DELETE", 331 | "header": [ 332 | { 333 | "key": "X-API-Token", 334 | "value": "vfuzd2nvaweojqolm4kq", 335 | "type": "text" 336 | }, 337 | { 338 | "key": "Content-Type", 339 | "value": "application/json", 340 | "type": "text" 341 | } 342 | ], 343 | "url": { 344 | "raw": "http://localhost:5000/token/jyrvm14k9tvdiesxwgku", 345 | "protocol": "http", 346 | "host": [ 347 | "localhost" 348 | ], 349 | "port": "5000", 350 | "path": [ 351 | "token", 352 | "jyrvm14k9tvdiesxwgku" 353 | ] 354 | } 355 | }, 356 | "response": [] 357 | } 358 | ], 359 | "protocolProfileBehavior": {} 360 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST API Goat 2 | 3 | This is a "Goat" project so you can get familiar with REST API testing. 4 | There is an included Postman project so you can see how everything is meant 5 | to be called. If you encounter any components of the API which don't work 6 | correctly, please create an Issue for them. 7 | 8 | ## How To: 9 | Make sure you can run `pipenv`. On Macs, this easily is installed once homebrew 10 | is installed. Hit up Teams for how to install homebrew (or Google it). 11 | 12 | 1. `pipenv shell` 13 | 2. `pip install flask` 14 | 3. `export FLASK_APP=server.py` 15 | 4. `flask run` 16 | 17 | If everything went ok, you should have the Goat running on localhost:5000. Try 18 | to load the following URL in your brower and look for a basic "Hello" page: 19 | 20 | http://localhost:5000/ 21 | 22 | ## How To (Docker) 23 | 24 | 1. `docker build -t rest-api-goat:latest .` 25 | 2. `docker run -d -p 5000:5000 rest-api-goat` 26 | 3. Test at http://localhost:5000/ 27 | 28 | ## Credentials 29 | You have been given the following credentials: 30 | 31 | | API Token | Company Number | 32 | |--------------------- | -------------: | 33 | | vfuzd2nvaweojqolm4kq | 1 | 34 | | ek9chlb4t96sncbr9dgx | 1 | 35 | | x6oici7wh3prgx34fxo1 | 2 | 36 | | 7eojwd75kqd80m4sm169 | 2 | 37 | | jyrvm14k9tvdiesxwgku | 3 | 38 | 39 | API tokens are typically provided as header values; see the Postman collection 40 | for examples of how these tokens are used. 41 | 42 | ## API overview: 43 | This API models a multi-tenant banking API. Banks have API tokens they use to 44 | authenticate with the API. Banks should only be able to see information about 45 | their own customers, and shouldn't be able to determine the balance of customers 46 | of another bank. 47 | 48 | The server-side code is written in Python (using Flask) and data persistence is 49 | handled by SQLite. (You can think of this as a SQL server.) 50 | 51 | ## API Methods: 52 | 53 | ### Basic Methods: 54 | #### Authenticate 55 | This is a basic method that checks if your token is good and tells you stuff 56 | about your current user. 57 | 58 | #### Get Customers 59 | This method shows you all the customers of your bank and their balances 60 | 61 | #### Get Customer v1 62 | Used to get information about one specific customer. It was not implemented 63 | correctly and a new developer was hired to fix its security problem. 64 | 65 | #### Get Customer v2 66 | This is the new version that the new developer claims fixes the issue from 67 | v1 and is now secure. Is it? 68 | 69 | #### Create Access Token 70 | Creates a new access token tied to the current user's company. A bank might use 71 | this to give each API client they own a unique ID. 72 | 73 | #### Delete Token 74 | Deletes an access token. (Known bug: you can delete all of a company's access 75 | tokens. If you do this and can't get back in, run `python install_db.py` to reset 76 | everthing.) 77 | 78 | ### Transfers: 79 | All transfers are a 3-step process. First, a transfer is created. It contains 80 | information about who is sending money, who is receiving it, and how much. 81 | 82 | Next, the banks wanted a way to batch all the transfers together. They thought 83 | this would be better than processing each one individually. Decisions about 84 | whether or not the transfer is valid are made here. 85 | 86 | Finally, transfers are completed individually. Why banks wanted this instead of 87 | batching the entire transaction together is not explained in any documentation. 88 | 89 | #### Create Transfer 90 | The money sender, money receiver, and ammount are submitted. The server will 91 | accept anything for which the JSON is valid and possesses the required information. 92 | Don't expect error checking at this step. 93 | 94 | #### Process Transfers 95 | Takes all transfers in the "CREATED" state and determines if they should be 96 | submitted or not. Returns a list of ID values which were accepted ("pending") 97 | and which ones were rejected ("failed"). Customer balances are not changed. 98 | 99 | #### Confirm Transfer 100 | Takes an ID value and completes the transfer if it's in the PENDING state. This 101 | step changes customer balances and transfers the money. 102 | 103 | #### Get Tranfers 104 | Shows a list of all transfers in the system and their state. API clients can 105 | also filter on state, such as "CREATED", "PENDING", or "COMPLETE". 106 | 107 | ## What to Test for: 108 | The answer to this question always starts with "What is this API's threat model?" 109 | If you're Billy Badguy, what kind of things would you want to do? Billy Badguy 110 | doesn't want to live in his friend's basement anymore, but he doesn't want a job 111 | either. How's he going to get rent money? 112 | 113 | See the "Guide" folder of this project for clues and hints. See what you can figure 114 | out without help first! 115 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "restgoat.db" 3 | } -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | with open("config.json") as config_file: 4 | config = json.load(config_file) -------------------------------------------------------------------------------- /guide/README.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | This guide is meant as a learning tool for when you get stuck. Try to 4 | figure out the objectives yourself first, before looking for hints in 5 | this guide. The clues are meant to give progressively more guidance 6 | to help you find the solution. This guide is *not* meant to give you 7 | the solution itself. If you find yourself still stuck after giving it 8 | several attempts, try to reach out for help in the Teams chat. You 9 | don't have to tell everyone that you're doing a Goat; pretend it's 10 | for a client 11 | 12 | **Note:** When doing an assessment, you aren't going to have a list 13 | of objectives/known issues to hunt for. See how many weaknesses you 14 | can find on your own first! 15 | 16 | ## Blackbox Testing 17 | 18 | #### Get Customer v1 19 |
20 | Objective 1 21 | Clue: What happens when you change the number at the end of this URL? 22 |
23 | 24 | #### Get Customer v2 25 |
26 | Objective 1 27 | Clue: What happens when you change the number at the end of this URL to a string? 28 |
29 | 30 | #### Create Transfer 31 |
32 | Objective 1 33 | Clue: Send $50 from one user to another. Now send $50 back, but don't change `to` and `from` 34 |
35 | 36 | #### Get Transfers (All) 37 |
38 | Objective 1 39 | No known issues with this endpoint, but if you find one let me know! 40 |
41 | 42 | #### Get Transfers (Created) 43 |
44 | Objective 1 45 | No known issues with this endpoint, but if you find one let me know! 46 |
47 | 48 | #### Authenticate 49 |
50 | Objective 1 51 | This is a complex objective. Analyze the application's source code and determine how `Create Access Token` works. 52 | If there were a large number of access tokens present in the system, could you determine which ones are real? 53 |
54 | 55 | ### Transfers 56 | This use may use `Create Transfer`, `Process Transfer`, `Confirm Transfer`, or a combination: 57 |
58 | Objective 1 59 | Clue: Use "Create Transfer" to make a transfer request for $100,000. What happens when you process 60 | this request? Can you bypass this constraint? 61 |
62 | 63 |
64 | Objective 2 65 | Clue: Objective 1 can be completed in at least two ways. If you used the solution to "Create Transfer" 66 | Objective 1, can you solve it some other way? What happens if Robert needs to make two transfers to Juan? 67 |
68 | 69 | 70 | ### No Known Issues: 71 | The following API methods do not have known issues. If you find one, please let me know! 72 | * Get Customers 73 | 74 | ### Others 75 | I haven't offered clues for all methods on purpose. Some offer a second change to practice one of the 76 | weaknesses above. See if you can find problems with them. (I'm also moving pretty fast and might have 77 | just forgotten some of them. Sorry!) 78 | 79 | ## Source Code 80 | Common source code assessment goals: 81 | * How does routing work? Can you identify any endpoints which aren't referenced in the Postman collect? 82 | * What are common strings to grep for? 83 | - Do any "TODO" comments shed light on known issues? 84 | - The API uses a SQL backend. Is SQL Injection possible? How would you find it in code? 85 | - Cross-Site Scripting might not apply to this application. 86 | * Can you spot any logic errors in the Transfer workflow? Has breaking it into 3 steps exposed it to any weaknesses? 87 | * Can you predict API tokens? (Note: The initial 5 tokens may not fit the pattern.) -------------------------------------------------------------------------------- /install_db.py: -------------------------------------------------------------------------------- 1 | import json, sqlite3 2 | import config 3 | 4 | db = sqlite3.connect(config.config['db_name']) 5 | 6 | db.execute('''DROP TABLE IF EXISTS users''') # Old case 7 | 8 | db.execute('''DROP TABLE IF EXISTS tokens''') 9 | db.execute('''CREATE TABLE tokens (id INTEGER PRIMARY KEY, api_token VARCHAR(20) UNIQUE, company INTEGER)''') 10 | db.execute('''INSERT INTO tokens VALUES (null, 'vfuzd2nvaweojqolm4kq', 1) ''') 11 | db.execute('''INSERT INTO tokens VALUES (null, 'ek9chlb4t96sncbr9dgx', 1) ''') 12 | db.execute('''INSERT INTO tokens VALUES (null, 'x6oici7wh3prgx34fxo1', 2) ''') 13 | db.execute('''INSERT INTO tokens VALUES (null, '7eojwd75kqd80m4sm169', 2) ''') 14 | db.execute('''INSERT INTO tokens VALUES (null, 'jyrvm14k9tvdiesxwgku', 3) ''') 15 | 16 | db.execute('''DROP TABLE IF EXISTS companies''') 17 | db.execute('''CREATE TABLE companies (id INTEGER PRIMARY KEY, name VARCHAR(50))''') 18 | db.execute('''INSERT INTO companies VALUES (1, 'MegaBank Inc.')''') 19 | db.execute('''INSERT INTO companies VALUES (2, 'Sketchy Steve''s InvestCorp')''') 20 | db.execute('''INSERT INTO companies VALUES (3, 'Nota Bank')''') 21 | 22 | db.execute('''DROP TABLE IF EXISTS customers''') 23 | db.execute('''CREATE TABLE customers (id INTEGER PRIMARY KEY, company INTEGER, balance DECIMAL, name TEXT)''') 24 | db.execute('''INSERT INTO customers VALUES (null, 3, 100.00, "Susan")''') 25 | db.execute('''INSERT INTO customers VALUES (null, 1, 1024.63, "Robert")''') 26 | db.execute('''INSERT INTO customers VALUES (null, 1, 651.20, "Juan")''') 27 | db.execute('''INSERT INTO customers VALUES (null, 2, 23651.20, "Arjun")''') 28 | db.execute('''INSERT INTO customers VALUES (null, 1, 12345.49, "Ataahua")''') 29 | 30 | db.execute('''DROP TABLE IF EXISTS transfers''') 31 | db.execute("CREATE TABLE transfers (id INTEGER PRIMARY KEY, custID_from INTEGER, custID_to INTEGER, amount DECIMAL, status VARCHAR(10) CHECK(status in ('CREATED', 'PENDING', 'COMPLETE'))) ") 32 | # This table intentionally left blank 33 | 34 | db.commit() # NEED this line or changes aren't made 35 | db.close() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.1 2 | -------------------------------------------------------------------------------- /restgoat.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optiv/rest-api-goat/d93d1d51b98655708bdd43f380ca3e8a789c6211/restgoat.db -------------------------------------------------------------------------------- /routes.py: -------------------------------------------------------------------------------- 1 | import json 2 | import server 3 | 4 | def row2dict(row): 5 | """Takes sqlite3.Row objects and converts them to dictionaries. 6 | This is important for JSON serialization because otherwise 7 | Python has no idea how to turn a sqlite3.Row into JSON.""" 8 | x = {} 9 | for col in row.keys(): 10 | x[col] = row[col] 11 | return x 12 | 13 | def check_token(api_token): 14 | # TODO: try some fancy JOIN for user + company 15 | c = server.get_db().cursor() 16 | c.execute("SELECT * FROM tokens WHERE api_token=? LIMIT 1", [api_token]) 17 | user = c.fetchone() 18 | if user is not None: 19 | response = {"success": True} 20 | response["user"] = row2dict(user) 21 | c.execute("SELECT * FROM companies WHERE id=? LIMIT 1", [user["company"]]) 22 | company = c.fetchone() 23 | response["company"] = {"name": company["name"], "id": company["id"]} 24 | else: 25 | response = {"success": False, "error": "Invalid API token."} 26 | return response 27 | 28 | @server.app.route('/authenticate', methods=['POST']) 29 | def authenticate(): 30 | content = server.request.json 31 | try: 32 | api_token = content["api_token"] 33 | response = check_token(api_token) 34 | return response 35 | except KeyError: 36 | response = {"success": False, "error": "No API token given."} 37 | return response 38 | 39 | @server.app.route('/get_customers') 40 | def get_customers(): 41 | api_token = server.request.headers.get('X-API-Token') 42 | auth = check_token(api_token) 43 | if not auth["success"]: 44 | return auth 45 | 46 | c = server.get_db().cursor() 47 | c.execute("SELECT * FROM customers WHERE company=?", [auth["company"]["id"]]) 48 | rows = c.fetchall() 49 | response = { 50 | "success": True, 51 | "customers": [] 52 | } 53 | 54 | for row in rows: 55 | x = row2dict(row) 56 | response["customers"].append(x) 57 | return response 58 | 59 | # goatnote: 60 | # SQLi 61 | @server.app.route('/get_customer_v2/') 62 | def get_customer2(id): 63 | api_token = server.request.headers.get('X-API-Token') 64 | auth = check_token(api_token) 65 | if not auth["success"]: 66 | return auth 67 | 68 | c = server.get_db().cursor() 69 | c.execute("SELECT * FROM customers WHERE company=" + str(auth["company"]["id"]) + " AND id=" + id) 70 | rows = c.fetchall() 71 | response = { 72 | "success": True, 73 | "customers": [] 74 | } 75 | 76 | for row in rows: 77 | x = {} 78 | for col in row.keys(): 79 | x[col] = row[col] 80 | response["customers"].append(x) 81 | return response 82 | 83 | # goatnote: 84 | # IDOR 85 | @server.app.route('/get_customer_v1/') 86 | def get_customer1(id): 87 | api_token = server.request.headers.get('X-API-Token') 88 | auth = check_token(api_token) 89 | if not auth["success"]: 90 | return auth 91 | 92 | c = server.get_db().cursor() 93 | c.execute("SELECT * FROM customers WHERE id=?", [id]) 94 | rows = c.fetchall() 95 | response = { 96 | "success": True, 97 | "customers": [] 98 | } 99 | 100 | for row in rows: 101 | x = {} 102 | for col in row.keys(): 103 | x[col] = row[col] 104 | response["customers"].append(x) 105 | return response 106 | 107 | @server.app.route('/transfer', methods=['PUT']) 108 | def create_transfer(): 109 | api_token = server.request.headers.get('X-API-Token') 110 | auth = check_token(api_token) 111 | if not auth["success"]: 112 | return auth 113 | 114 | content = server.request.json 115 | if content is None: 116 | response = {"success": False, "error": "Expected JSON content"} 117 | return response 118 | try: 119 | cust_from = int(content["from"]) 120 | cust_to = int(content["to"]) 121 | ammount = content["ammount"] 122 | except KeyError: 123 | response = {"success": False, "error": "Missing 'from', 'to', or 'ammount'"} 124 | return response 125 | except ValueError: 126 | response = {"success": False, "error": "Values of 'from' and 'to' should be customer IDs."} 127 | return response 128 | 129 | # Decimals maintain more accuracy than floats (or should) so we don't want 130 | # this number as a float. However, all decimals should be valid floats. 131 | # TODO: Python has a decimal type, should probably use that? 132 | try: 133 | float(ammount) 134 | except ValueError: 135 | response = {"success": False, "error": "Value of 'ammount' should be a decimal number"} 136 | return response 137 | 138 | # goatnote: 139 | # Not doing: validation of cust_from or cust_to 140 | db = server.get_db() 141 | c = db.cursor() 142 | try: 143 | c.execute('INSERT INTO transfers VALUES (null, ?, ?, ?, ?)', [cust_from, cust_to, ammount, 'CREATED']) 144 | db.commit() 145 | except: 146 | response = {"success": False, "error": "Unknown SQL error"} 147 | raise 148 | return response 149 | 150 | response = {"success": True, "id": c.lastrowid} 151 | return response 152 | 153 | 154 | @server.app.route('/get_transfers') 155 | @server.app.route('/get_transfers/') 156 | def get_transfers(status=None): 157 | api_token = server.request.headers.get('X-API-Token') 158 | auth = check_token(api_token) 159 | if not auth["success"]: 160 | return auth 161 | 162 | c = server.get_db().cursor() 163 | if status is None: 164 | c.execute("SELECT * FROM transfers") 165 | else: 166 | c.execute("SELECT * FROM transfers WHERE status=?", [status]) 167 | rows = c.fetchall() 168 | response = { 169 | "success": True, 170 | "transfers": [], 171 | "where": status 172 | } 173 | 174 | for row in rows: 175 | x = {} 176 | for col in row.keys(): 177 | x[col] = row[col] 178 | response["transfers"].append(x) 179 | return response 180 | 181 | @server.app.route('/process_transfers/', methods=['POST']) 182 | def process_transfers(): 183 | """Advances all CREATED transfers to pending if they look ok. 184 | Will not complete the transfer; use /confirm-transfer/ for that.""" 185 | 186 | api_token = server.request.headers.get('X-API-Token') 187 | auth = check_token(api_token) 188 | if not auth["success"]: 189 | return auth 190 | 191 | db = server.get_db() 192 | c = db.cursor() 193 | transfers = c.execute("SELECT * FROM transfers WHERE status=?", ['CREATED']).fetchall() 194 | #x = [] 195 | #for row in transfers: 196 | # x.append(row2dict(row)) 197 | #return {"data": x} 198 | 199 | ok = [] 200 | error = [] 201 | # goatnote: 202 | # Logic error here allows balances to become overdrawn 203 | for xfer in transfers: 204 | balance = c.execute("SELECT balance from customers WHERE id=?", [xfer["custID_from"]]).fetchone() 205 | if balance["balance"] > xfer["amount"]: 206 | c.execute("UPDATE transfers SET status=? WHERE id=?", ['PENDING', xfer['id']]) 207 | ok.append(xfer['id']) 208 | else: 209 | error.append(xfer['id']) 210 | response = {"success": True, "pending": ok, "failed": error} 211 | 212 | db.commit() 213 | return response 214 | 215 | # What even is database optimization? 216 | @server.app.route('/confirm_transfer/', methods=['POST']) 217 | def confirm_transfer(xfer_id): 218 | 219 | db = server.get_db() 220 | c = db.cursor() 221 | 222 | try: 223 | xfer_id = int(xfer_id) 224 | except ValueError: 225 | response = {"success": False, "error": "Transfer ID must be an integer"} 226 | return response 227 | 228 | xfer = c.execute("SELECT * FROM transfers WHERE id=? LIMIT 1", [xfer_id]).fetchone() 229 | if xfer is None: 230 | response = {"success": False, "error": "Transfer ID invalid"} 231 | return response 232 | 233 | c.execute("UPDATE transfers SET status=? WHERE id=? AND status=?", ['COMPLETE', xfer['id'], 'PENDING']) 234 | if c.rowcount == 0: 235 | response = {"success": False, "error": "Transfer ID {} was not in the 'PENDING' state".format(xfer_id)} 236 | return response 237 | else: 238 | customer_to = c.execute("SELECT * from customers WHERE id=?", [xfer['custID_to']]).fetchone() 239 | customer_from = c.execute("SELECT * from customers WHERE id=?", [xfer['custID_from']]).fetchone() 240 | 241 | new_balance_from = customer_from['balance'] - xfer['amount'] 242 | c.execute('UPDATE customers SET balance=? WHERE id=?', [new_balance_from, xfer['custID_from']]) 243 | 244 | new_balance_to = customer_to['balance'] + xfer['amount'] 245 | c.execute('UPDATE customers SET balance=? WHERE id=?', [new_balance_to, xfer['custID_to']]) 246 | 247 | response = {"success": True} 248 | 249 | db.commit() 250 | return response 251 | 252 | @server.app.route('/token/', methods=['DELETE']) 253 | def delete_token(token): 254 | api_token = server.request.headers.get('X-API-Token') 255 | auth = check_token(api_token) 256 | if not auth["success"]: 257 | return auth 258 | 259 | db = server.get_db() 260 | c = db.cursor() 261 | 262 | # Crazy logic to say "Delete only if there's at least 1 more token for this company." 263 | c.execute('DELETE FROM tokens WHERE api_token=?', [token]) 264 | 265 | db.commit() 266 | if c.rowcount > 0: 267 | result = {"success": True} 268 | else: 269 | result = {"success": False, "error": "No such token"} 270 | return result 271 | 272 | 273 | @server.app.route('/new_token', methods=["POST", "PUT"]) 274 | def assign_new_token(): 275 | """Builds a new API token and assigns it to the current user's 276 | company. If a bank wanted each client to use a unique token, 277 | perhaps so that they can identify which API client from logged 278 | data, they might use this method. 279 | 280 | If you think this method looks too sloppy to be real code, 281 | you don't do enough source code assessments.""" 282 | 283 | from hashlib import sha256 284 | 285 | api_token = server.request.headers.get('X-API-Token') 286 | auth = check_token(api_token) 287 | if not auth["success"]: 288 | return auth 289 | 290 | db = server.get_db() 291 | c = db.cursor() 292 | 293 | # TODO: SHA256 is secure, but is there a better way to do this? 294 | row = c.execute('SELECT MAX(id) FROM tokens').fetchone() 295 | old_token_id = row[0] 296 | 297 | # Hashing methods only take bytes objects. 298 | # All this says is "give me '5' as a byte array" 299 | num = bytes(str(old_token_id), 'utf-8') 300 | h = sha256() 301 | h.update(num) 302 | out = h.digest() 303 | 304 | # We don't want bytes, we want 20-character strings using 0-9 and a-z 305 | # This causes some truncation and loss of entropy 306 | # TODO: longer tokens to maintain entropy? 307 | out = out[:20] 308 | out_modulo = [int(x) % 36 for x in out] # 36 = 26 letters + 10 digits 309 | 310 | for i in range(len(out_modulo)): 311 | if out_modulo[i] < 10: 312 | out_modulo[i] = str(out_modulo[i]) 313 | else: 314 | out_modulo[i] = chr(ord('a') + out_modulo[i] - 10) 315 | 316 | new_token = ''.join(out_modulo) 317 | 318 | c.execute('INSERT INTO tokens VALUES (null, ?, ?)', [new_token, auth["user"]["company"]]) 319 | db.commit() 320 | 321 | result = { 322 | "success": True, 323 | "id": c.lastrowid, 324 | "token": new_token 325 | } 326 | 327 | return result 328 | 329 | 330 | @server.app.route('/get_company/') 331 | def get_company(comp_id): 332 | c = server.get_db().cursor() 333 | company = c.execute("SELECT * FROM companies WHERE id=?", [comp_id]).fetchone() 334 | 335 | if company is not None: 336 | result = {"success": True, "data": row2dict(company)} 337 | return result 338 | else: 339 | result = {"success": False, "error": "Company ID {} not found".format(comp_id)} 340 | return result 341 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from flask import Flask, escape, request, g 3 | import config 4 | 5 | app = Flask(__name__) 6 | 7 | # Due to sqlite3/Flask threading issues, we need this semi-Singleton 8 | def get_db(): 9 | db = getattr(g, '_database', None) 10 | if db is None: 11 | # isolation_level=None means that all INSERTS/UPDATES 12 | # take place immediately, no need to .commit() them. 13 | #db = g._database = sqlite3.connect(config.config['db_name'], isolation_level=None) 14 | db = g._database = sqlite3.connect(config.config['db_name']) 15 | 16 | # This allows us to get row results by name 17 | db.row_factory = sqlite3.Row 18 | return db 19 | 20 | @app.route('/') 21 | def hello(): 22 | name = request.args.get("name", "World") 23 | return f'Hello, {escape(name)}!' 24 | 25 | @app.teardown_appcontext 26 | def close_connection(exception): 27 | db = getattr(g, '_database', None) 28 | if db is not None: 29 | db.close() 30 | 31 | import routes --------------------------------------------------------------------------------