├── .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
--------------------------------------------------------------------------------