├── NaNissue.pptx ├── README.md ├── lib └── nan_safety.py └── woohoo ├── Dockerfile ├── Ducktales.postman_collection.json ├── README.md ├── db.sqlite3 ├── ducktales ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-39.pyc │ ├── settings.cpython-39.pyc │ ├── urls.cpython-39.pyc │ └── wsgi.cpython-39.pyc ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── notaduck ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-39.pyc │ ├── urls.cpython-39.pyc │ └── views.cpython-39.pyc ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py └── requirements.txt /NaNissue.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/NaNissue.pptx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ducktales - woohoo! 2 | 3 | ## Validation library by @RSNAKE 4 | `./lib/nan_safety.py` 5 | 6 | ## Vulnerable Djanjo "API" 7 | `./woohoo` 8 | Illustrates some of the failure modes described by the presentation, see its readme or the postman collections for how. 9 | 10 | -------------------------------------------------------------------------------- /lib/nan_safety.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ## Written by Robert "RSnake" Hansen to fix the nanissue for Python 3 | ## It makes certain primatives like addition, subtraction, etc. available 4 | ## so that one can safely perform operations on ints and floats without 5 | ## producing NaN and inf inadvertantly, or doing unsafe operations on 6 | ## them. 7 | ## 8 | ## Whereerver you see "safe" if it is True it will crash as opposed to 9 | ## continuing as safely as it can while throwing an error. 10 | 11 | ## Identifies the presence of NaN by asking if the variable equals itself 12 | def identify_nan(variable): 13 | try: 14 | if float(variable) == float(variable): 15 | return 0 16 | else: 17 | return 1 18 | except: 19 | return 0 20 | 21 | ## Identifies the presence of an inf or negative inf by adding a float and seeing if it equals itself 22 | def identify_inf(variable): 23 | try: 24 | if float(variable) + 1 != float(variable): 25 | return 0 26 | else: 27 | return 1 28 | except: 29 | return 0 30 | 31 | ## Attempts to see if variable is either an inf -inf or NaN 32 | def test_var_safety(test_variable, returntype, safe): 33 | try: 34 | if returntype == 'float': 35 | return_var = float(test_variable) 36 | else: 37 | float(test_variable) 38 | return_var = test_variable 39 | except: 40 | if safe: 41 | raise RuntimeError ("Variable cannot safely be converted to float.") 42 | 43 | error = 0 44 | if identify_nan(return_var): 45 | if safe: 46 | raise RuntimeError ("Encountered a NaN in safe mode.") 47 | else: 48 | error = 1 49 | 50 | if identify_inf(return_var): 51 | if safe: 52 | raise RuntimeError ("Encountered an inf in safe mode.") 53 | else: 54 | error = 1 55 | 56 | return return_var, error 57 | 58 | ## Safely allows the user to sum two or more integers/floats together in a list 59 | def safely_sum (thelist, safe): 60 | testsum = 0 61 | for thisvar in thelist: 62 | testsum = testsum + thisvar 63 | return test_var_safety(testsum, type(testsum), safe) 64 | 65 | ## Safely allows the user to subtract two or more integers/floats together in a list 66 | def safely_subtract (thelist, safe): 67 | testsum = 0 68 | for thisvar in thelist: 69 | testsum = testsum - thisvar 70 | return test_var_safety(testsum, type(testsum), safe) 71 | 72 | ## Safely allows the user to average two or more integers/floats together in a list 73 | def safely_average (thelist, safe): 74 | sum, error = safely_sum(thelist, safe) 75 | return sum/len(thelist), error 76 | 77 | ## Safely allows the user to multiply two integers/floats together in a list 78 | def safely_multiply (first, second, safe): 79 | testsum = first * second 80 | return test_var_safety(testsum, type(testsum), safe) 81 | 82 | ## Safely allows the user to divide two integers/floats together 83 | def safely_divide (first, second, safe): 84 | test = 0 85 | try: 86 | test = first / second 87 | except: 88 | if safe: 89 | raise RuntimeError ("Non float/integer found in safe mode.") 90 | else: 91 | return 0, 1 92 | 93 | if type(test) is float: 94 | returnvar, error = test_var_safety(test, 'float', safe) 95 | else: 96 | returnvar, error = test_var_safety(test, 'int', safe) 97 | 98 | return returnvar, error 99 | 100 | 101 | ## Some examples: 102 | # asdf = 'inf' 103 | # returnvar, error = test_var_safety(asdf, 'float', True) 104 | # 105 | # asdf = [123, float('Nan'), 4] 106 | # returnvar, error = safely_average(asdf, True) 107 | # 108 | # returnvar, error = safely_divide(3e3333, 1, True) 109 | # print (returnvar) 110 | # print (error) 111 | -------------------------------------------------------------------------------- /woohoo/Dockerfile: -------------------------------------------------------------------------------- 1 | from python:3-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install -r requirements.txt 7 | COPY . . 8 | 9 | EXPOSE 8000 10 | CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] 11 | -------------------------------------------------------------------------------- /woohoo/Ducktales.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "9e468546-4752-498e-b342-fcfb84d8ac87", 4 | "name": "Ducktales", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Index (Playground)", 10 | "item": [ 11 | { 12 | "name": "Basic Request", 13 | "event": [ 14 | { 15 | "listen": "test", 16 | "script": { 17 | "exec": [ 18 | "pm.test(\"Status code is 200\", function () {", 19 | " pm.response.to.have.status(200);", 20 | "});", 21 | "", 22 | "pm.test(\"Body contains ints\", function () {", 23 | " var jsonData = pm.response.json();", 24 | " pm.expect(jsonData.foo.type).to.eql(\"int\");", 25 | " pm.expect(jsonData.q.type).to.eql(\"int\");", 26 | "});" 27 | ], 28 | "type": "text/javascript" 29 | } 30 | } 31 | ], 32 | "request": { 33 | "method": "POST", 34 | "header": [], 35 | "body": { 36 | "mode": "raw", 37 | "raw": "{\"foo\": 123, \"q\": 123}", 38 | "options": { 39 | "raw": { 40 | "language": "json" 41 | } 42 | } 43 | }, 44 | "url": { 45 | "raw": "localhost:8000/notaduck/?vara=1&varb=1", 46 | "host": [ 47 | "localhost" 48 | ], 49 | "port": "8000", 50 | "path": [ 51 | "notaduck", 52 | "" 53 | ], 54 | "query": [ 55 | { 56 | "key": "vara", 57 | "value": "1" 58 | }, 59 | { 60 | "key": "varb", 61 | "value": "1" 62 | } 63 | ] 64 | } 65 | }, 66 | "response": [] 67 | }, 68 | { 69 | "name": "Foo is divided by Q", 70 | "event": [ 71 | { 72 | "listen": "test", 73 | "script": { 74 | "exec": [ 75 | "pm.test(\"Status code is 200\", function () {", 76 | " pm.response.to.have.status(200);", 77 | "});", 78 | "", 79 | "pm.test(\"40/20 = 2\", function () {", 80 | " var jsonData = pm.response.json();", 81 | " pm.expect(jsonData.foo.math).to.eql(2);", 82 | "});" 83 | ], 84 | "type": "text/javascript" 85 | } 86 | } 87 | ], 88 | "request": { 89 | "method": "POST", 90 | "header": [], 91 | "body": { 92 | "mode": "raw", 93 | "raw": "{\"foo\": 40, \"q\": 20}", 94 | "options": { 95 | "raw": { 96 | "language": "json" 97 | } 98 | } 99 | }, 100 | "url": { 101 | "raw": "localhost:8000/notaduck/?vara=1&varb=1", 102 | "host": [ 103 | "localhost" 104 | ], 105 | "port": "8000", 106 | "path": [ 107 | "notaduck", 108 | "" 109 | ], 110 | "query": [ 111 | { 112 | "key": "vara", 113 | "value": "1" 114 | }, 115 | { 116 | "key": "varb", 117 | "value": "1" 118 | } 119 | ] 120 | } 121 | }, 122 | "response": [] 123 | }, 124 | { 125 | "name": "\"foo\" becomes a float", 126 | "event": [ 127 | { 128 | "listen": "test", 129 | "script": { 130 | "exec": [ 131 | "pm.test(\"Status code is 200\", function () {", 132 | " pm.response.to.have.status(200);", 133 | "});", 134 | "" 135 | ], 136 | "type": "text/javascript" 137 | } 138 | } 139 | ], 140 | "request": { 141 | "method": "POST", 142 | "header": [], 143 | "body": { 144 | "mode": "raw", 145 | "raw": "{\"foo\": 123.00, \"q\": 123}", 146 | "options": { 147 | "raw": { 148 | "language": "json" 149 | } 150 | } 151 | }, 152 | "url": { 153 | "raw": "localhost:8000/notaduck/?vara=1&varb=1", 154 | "host": [ 155 | "localhost" 156 | ], 157 | "port": "8000", 158 | "path": [ 159 | "notaduck", 160 | "" 161 | ], 162 | "query": [ 163 | { 164 | "key": "vara", 165 | "value": "1" 166 | }, 167 | { 168 | "key": "varb", 169 | "value": "1" 170 | } 171 | ] 172 | } 173 | }, 174 | "response": [] 175 | }, 176 | { 177 | "name": "\"foo\" becomes a \"float\"", 178 | "event": [ 179 | { 180 | "listen": "test", 181 | "script": { 182 | "exec": [ 183 | "pm.test(\"Status code is 200\", function () {", 184 | " pm.response.to.have.status(200);", 185 | "});", 186 | "", 187 | "pm.test(\"Math is now NaN, which actually (should) breaks JavaScript/JSON\", function () {", 188 | " //just trying to read the response will error because NaN isn't valid json.", 189 | " var jsonData = pm.response.json();", 190 | " //pm.expect(jsonData.foo.math).to.eql(jsonData.foo.math);", 191 | "});", 192 | "" 193 | ], 194 | "type": "text/javascript" 195 | } 196 | } 197 | ], 198 | "request": { 199 | "method": "POST", 200 | "header": [], 201 | "body": { 202 | "mode": "raw", 203 | "raw": "{\"foo\": NaN, \"q\": 123}", 204 | "options": { 205 | "raw": { 206 | "language": "json" 207 | } 208 | } 209 | }, 210 | "url": { 211 | "raw": "localhost:8000/notaduck/?vara=1&varb=1", 212 | "host": [ 213 | "localhost" 214 | ], 215 | "port": "8000", 216 | "path": [ 217 | "notaduck", 218 | "" 219 | ], 220 | "query": [ 221 | { 222 | "key": "vara", 223 | "value": "1" 224 | }, 225 | { 226 | "key": "varb", 227 | "value": "1" 228 | } 229 | ] 230 | } 231 | }, 232 | "response": [] 233 | } 234 | ], 235 | "description": "The base URL loads two query parameters, and two json body attributes and then applies various casts and math operations, it outputs the results (and types) so that you can get a feel for how NaN and INFs are handled." 236 | }, 237 | { 238 | "name": "Book Ratings", 239 | "item": [ 240 | { 241 | "name": "Zach rates book1 five stars", 242 | "event": [ 243 | { 244 | "listen": "test", 245 | "script": { 246 | "exec": [ 247 | "pm.test(\"Book1 is highest rated with 5 stars\", function () {", 248 | " var jsonData = pm.response.json();", 249 | " pm.expect(jsonData.book1).to.eql(5);", 250 | " pm.expect(jsonData.highest_rated).to.eql(\"book1\");", 251 | "});" 252 | ], 253 | "type": "text/javascript" 254 | } 255 | }, 256 | { 257 | "listen": "prerequest", 258 | "script": { 259 | "exec": [ 260 | "const cookieJar = pm.cookies.jar();", 261 | "cookieJar.clear(\"http://localhost/notaduck/rate\", function (error){", 262 | " //err", 263 | "});" 264 | ], 265 | "type": "text/javascript" 266 | } 267 | } 268 | ], 269 | "request": { 270 | "method": "POST", 271 | "header": [], 272 | "body": { 273 | "mode": "raw", 274 | "raw": "{\"book\": \"book1\", \"user\": \"Zach\", \"rating\": 5}", 275 | "options": { 276 | "raw": { 277 | "language": "json" 278 | } 279 | } 280 | }, 281 | "url": { 282 | "raw": "localhost:8000/notaduck/rate/", 283 | "host": [ 284 | "localhost" 285 | ], 286 | "port": "8000", 287 | "path": [ 288 | "notaduck", 289 | "rate", 290 | "" 291 | ] 292 | } 293 | }, 294 | "response": [] 295 | }, 296 | { 297 | "name": "Robert rates book1 four stars", 298 | "event": [ 299 | { 300 | "listen": "test", 301 | "script": { 302 | "exec": [ 303 | "pm.test(\"Book1 is highest rated with average of 4.5 stars\", function () {", 304 | " var jsonData = pm.response.json();", 305 | " pm.expect(jsonData.book1).to.eql(4.5);", 306 | " pm.expect(jsonData.highest_rated).to.eql(\"book1\");", 307 | "});" 308 | ], 309 | "type": "text/javascript" 310 | } 311 | } 312 | ], 313 | "request": { 314 | "method": "POST", 315 | "header": [], 316 | "body": { 317 | "mode": "raw", 318 | "raw": "{\"book\": \"book1\", \"user\": \"Robert\", \"rating\": 4}", 319 | "options": { 320 | "raw": { 321 | "language": "json" 322 | } 323 | } 324 | }, 325 | "url": { 326 | "raw": "localhost:8000/notaduck/rate/", 327 | "host": [ 328 | "localhost" 329 | ], 330 | "port": "8000", 331 | "path": [ 332 | "notaduck", 333 | "rate", 334 | "" 335 | ] 336 | } 337 | }, 338 | "response": [] 339 | }, 340 | { 341 | "name": "Robert rates book2 one star", 342 | "event": [ 343 | { 344 | "listen": "test", 345 | "script": { 346 | "exec": [ 347 | "pm.test(\"Book1 is highest rated with average of 4.5 stars, book2 1 star\", function () {", 348 | " var jsonData = pm.response.json();", 349 | " pm.expect(jsonData.book1).to.eql(4.5);", 350 | " pm.expect(jsonData.book2).to.eql(1);", 351 | " pm.expect(jsonData.highest_rated).to.eql(\"book1\");", 352 | "});" 353 | ], 354 | "type": "text/javascript" 355 | } 356 | } 357 | ], 358 | "request": { 359 | "method": "POST", 360 | "header": [], 361 | "body": { 362 | "mode": "raw", 363 | "raw": "{\"book\": \"book2\", \"user\": \"Robert\", \"rating\": 1}", 364 | "options": { 365 | "raw": { 366 | "language": "json" 367 | } 368 | } 369 | }, 370 | "url": { 371 | "raw": "localhost:8000/notaduck/rate/", 372 | "host": [ 373 | "localhost" 374 | ], 375 | "port": "8000", 376 | "path": [ 377 | "notaduck", 378 | "rate", 379 | "" 380 | ] 381 | } 382 | }, 383 | "response": [] 384 | }, 385 | { 386 | "name": "Zach rates book2 two stars", 387 | "event": [ 388 | { 389 | "listen": "test", 390 | "script": { 391 | "exec": [ 392 | "pm.test(\"Book1 is highest rated with average of 4.5 stars, book2 1.5 star avg\", function () {", 393 | " var jsonData = pm.response.json();", 394 | " pm.expect(jsonData.book1).to.eql(4.5);", 395 | " pm.expect(jsonData.book2).to.eql(1.5);", 396 | " pm.expect(jsonData.highest_rated).to.eql(\"book1\");", 397 | "});" 398 | ], 399 | "type": "text/javascript" 400 | } 401 | } 402 | ], 403 | "request": { 404 | "method": "POST", 405 | "header": [], 406 | "body": { 407 | "mode": "raw", 408 | "raw": "{\"book\": \"book2\", \"user\": \"Zach\", \"rating\": 2}", 409 | "options": { 410 | "raw": { 411 | "language": "json" 412 | } 413 | } 414 | }, 415 | "url": { 416 | "raw": "localhost:8000/notaduck/rate/", 417 | "host": [ 418 | "localhost" 419 | ], 420 | "port": "8000", 421 | "path": [ 422 | "notaduck", 423 | "rate", 424 | "" 425 | ] 426 | } 427 | }, 428 | "response": [] 429 | }, 430 | { 431 | "name": "Hacker rates book2 five stars", 432 | "event": [ 433 | { 434 | "listen": "test", 435 | "script": { 436 | "exec": [ 437 | "pm.test(\"Hacker has no power here! Book1 is highest rated with average of 4.5 stars, book2 2.6 star avg\", function () {", 438 | " var jsonData = pm.response.json();", 439 | " pm.expect(jsonData.book1).to.eql(4.5);", 440 | " pm.expect(jsonData.book2).to.be.closeTo(2.666, 0.001)", 441 | " pm.expect(jsonData.highest_rated).to.eql(\"book1\");", 442 | "});" 443 | ], 444 | "type": "text/javascript" 445 | } 446 | } 447 | ], 448 | "request": { 449 | "method": "POST", 450 | "header": [], 451 | "body": { 452 | "mode": "raw", 453 | "raw": "{\"book\": \"book2\", \"user\": \"Hacker\", \"rating\": 5}", 454 | "options": { 455 | "raw": { 456 | "language": "json" 457 | } 458 | } 459 | }, 460 | "url": { 461 | "raw": "localhost:8000/notaduck/rate/", 462 | "host": [ 463 | "localhost" 464 | ], 465 | "port": "8000", 466 | "path": [ 467 | "notaduck", 468 | "rate", 469 | "" 470 | ] 471 | } 472 | }, 473 | "response": [] 474 | }, 475 | { 476 | "name": "Hacker rates book1 NaN stars", 477 | "event": [ 478 | { 479 | "listen": "test", 480 | "script": { 481 | "exec": [ 482 | "pm.test(\"WAT??? Book2? is highest rated?\", function () {", 483 | " var jsonData = pm.response.json();", 484 | " pm.expect(jsonData.book1).to.eql(4.5);", 485 | " pm.expect(jsonData.book2).to.eql(2.6);", 486 | " pm.expect(jsonData.highest_rated).to.eql(\"book1\");", 487 | "});" 488 | ], 489 | "type": "text/javascript" 490 | } 491 | } 492 | ], 493 | "request": { 494 | "method": "POST", 495 | "header": [], 496 | "body": { 497 | "mode": "raw", 498 | "raw": "{\"book\": \"book1\", \"user\": \"Hacker\", \"rating\": NaN}", 499 | "options": { 500 | "raw": { 501 | "language": "json" 502 | } 503 | } 504 | }, 505 | "url": { 506 | "raw": "localhost:8000/notaduck/rate/", 507 | "host": [ 508 | "localhost" 509 | ], 510 | "port": "8000", 511 | "path": [ 512 | "notaduck", 513 | "rate", 514 | "" 515 | ] 516 | } 517 | }, 518 | "response": [] 519 | } 520 | ], 521 | "auth": { 522 | "type": "noauth" 523 | }, 524 | "event": [ 525 | { 526 | "listen": "prerequest", 527 | "script": { 528 | "type": "text/javascript", 529 | "exec": [ 530 | "" 531 | ] 532 | } 533 | }, 534 | { 535 | "listen": "test", 536 | "script": { 537 | "type": "text/javascript", 538 | "exec": [ 539 | "" 540 | ] 541 | } 542 | } 543 | ] 544 | }, 545 | { 546 | "name": "BlindBid", 547 | "item": [ 548 | { 549 | "name": "Zach bids $10", 550 | "event": [ 551 | { 552 | "listen": "prerequest", 553 | "script": { 554 | "exec": [ 555 | "const cookieJar = pm.cookies.jar();", 556 | "cookieJar.clear(\"http://localhost/notaduck/rate\", function (error){", 557 | " //err", 558 | "});" 559 | ], 560 | "type": "text/javascript" 561 | } 562 | }, 563 | { 564 | "listen": "test", 565 | "script": { 566 | "exec": [ 567 | "pm.test(\"Status code is 200\", function () {", 568 | " pm.response.to.have.status(200);", 569 | "});" 570 | ], 571 | "type": "text/javascript" 572 | } 573 | } 574 | ], 575 | "request": { 576 | "method": "POST", 577 | "header": [], 578 | "body": { 579 | "mode": "raw", 580 | "raw": "{\n \"user\": \"Zach\",\n \"bid\": 10\n}", 581 | "options": { 582 | "raw": { 583 | "language": "json" 584 | } 585 | } 586 | }, 587 | "url": { 588 | "raw": "localhost:8000/notaduck/blindbids/", 589 | "host": [ 590 | "localhost" 591 | ], 592 | "port": "8000", 593 | "path": [ 594 | "notaduck", 595 | "blindbids", 596 | "" 597 | ] 598 | } 599 | }, 600 | "response": [] 601 | }, 602 | { 603 | "name": "Zach is winning", 604 | "event": [ 605 | { 606 | "listen": "test", 607 | "script": { 608 | "exec": [ 609 | "pm.test(\"Zach is Winning\", function () {", 610 | " var jsonData = pm.response.json();", 611 | " pm.expect(jsonData.winner).to.contain(\"Zach\")", 612 | "});" 613 | ], 614 | "type": "text/javascript" 615 | } 616 | } 617 | ], 618 | "request": { 619 | "method": "GET", 620 | "header": [], 621 | "url": { 622 | "raw": "localhost:8000/notaduck/whowon/", 623 | "host": [ 624 | "localhost" 625 | ], 626 | "port": "8000", 627 | "path": [ 628 | "notaduck", 629 | "whowon", 630 | "" 631 | ] 632 | } 633 | }, 634 | "response": [] 635 | }, 636 | { 637 | "name": "Robert bids $100", 638 | "event": [ 639 | { 640 | "listen": "test", 641 | "script": { 642 | "exec": [ 643 | "pm.test(\"Status code is 200\", function () {", 644 | " pm.response.to.have.status(200);", 645 | "});" 646 | ], 647 | "type": "text/javascript" 648 | } 649 | } 650 | ], 651 | "request": { 652 | "method": "POST", 653 | "header": [], 654 | "body": { 655 | "mode": "raw", 656 | "raw": "{\n \"user\": \"Robert\",\n \"bid\": 100\n}", 657 | "options": { 658 | "raw": { 659 | "language": "json" 660 | } 661 | } 662 | }, 663 | "url": { 664 | "raw": "localhost:8000/notaduck/blindbids/", 665 | "host": [ 666 | "localhost" 667 | ], 668 | "port": "8000", 669 | "path": [ 670 | "notaduck", 671 | "blindbids", 672 | "" 673 | ] 674 | } 675 | }, 676 | "response": [] 677 | }, 678 | { 679 | "name": "Robert is winning", 680 | "event": [ 681 | { 682 | "listen": "test", 683 | "script": { 684 | "exec": [ 685 | "pm.test(\"Robert is Winning\", function () {", 686 | " var jsonData = pm.response.json();", 687 | " pm.expect(jsonData.winner).to.contain(\"Robert\")", 688 | "});" 689 | ], 690 | "type": "text/javascript" 691 | } 692 | } 693 | ], 694 | "request": { 695 | "method": "GET", 696 | "header": [], 697 | "url": { 698 | "raw": "localhost:8000/notaduck/whowon/", 699 | "host": [ 700 | "localhost" 701 | ], 702 | "port": "8000", 703 | "path": [ 704 | "notaduck", 705 | "whowon", 706 | "" 707 | ] 708 | } 709 | }, 710 | "response": [] 711 | }, 712 | { 713 | "name": "Hacker uses Burner Bid", 714 | "event": [ 715 | { 716 | "listen": "test", 717 | "script": { 718 | "exec": [ 719 | "pm.test(\"Status code is 200\", function () {", 720 | " pm.response.to.have.status(200);", 721 | "});" 722 | ], 723 | "type": "text/javascript" 724 | } 725 | } 726 | ], 727 | "request": { 728 | "method": "POST", 729 | "header": [], 730 | "body": { 731 | "mode": "raw", 732 | "raw": "{\n \"user\": \"NannyBooBoo\",\n \"bid\": NaN\n}", 733 | "options": { 734 | "raw": { 735 | "language": "json" 736 | } 737 | } 738 | }, 739 | "url": { 740 | "raw": "localhost:8000/notaduck/blindbids/", 741 | "host": [ 742 | "localhost" 743 | ], 744 | "port": "8000", 745 | "path": [ 746 | "notaduck", 747 | "blindbids", 748 | "" 749 | ] 750 | } 751 | }, 752 | "response": [] 753 | }, 754 | { 755 | "name": "Hacker bids $0.1 for himself next", 756 | "event": [ 757 | { 758 | "listen": "test", 759 | "script": { 760 | "exec": [ 761 | "pm.test(\"Status code is 200\", function () {", 762 | " pm.response.to.have.status(200);", 763 | "});" 764 | ], 765 | "type": "text/javascript" 766 | } 767 | } 768 | ], 769 | "request": { 770 | "method": "POST", 771 | "header": [], 772 | "body": { 773 | "mode": "raw", 774 | "raw": "{\n \"user\": \"Hacker\",\n \"bid\": 0.1\n}", 775 | "options": { 776 | "raw": { 777 | "language": "json" 778 | } 779 | } 780 | }, 781 | "url": { 782 | "raw": "localhost:8000/notaduck/blindbids/", 783 | "host": [ 784 | "localhost" 785 | ], 786 | "port": "8000", 787 | "path": [ 788 | "notaduck", 789 | "blindbids", 790 | "" 791 | ] 792 | } 793 | }, 794 | "response": [] 795 | }, 796 | { 797 | "name": "Hacker WINS!", 798 | "event": [ 799 | { 800 | "listen": "test", 801 | "script": { 802 | "exec": [ 803 | "pm.test(\"Hacker Wins!\", function () {", 804 | " var jsonData = pm.response.json();", 805 | " pm.expect(jsonData.winner).to.contain(\"Hacker\")", 806 | "});" 807 | ], 808 | "type": "text/javascript" 809 | } 810 | } 811 | ], 812 | "request": { 813 | "method": "GET", 814 | "header": [], 815 | "url": { 816 | "raw": "localhost:8000/notaduck/whowon/", 817 | "host": [ 818 | "localhost" 819 | ], 820 | "port": "8000", 821 | "path": [ 822 | "notaduck", 823 | "whowon", 824 | "" 825 | ] 826 | } 827 | }, 828 | "response": [] 829 | } 830 | ] 831 | }, 832 | { 833 | "name": "Messages", 834 | "item": [ 835 | { 836 | "name": "localhost:8000/notaduck/messages/", 837 | "request": { 838 | "method": "POST", 839 | "header": [], 840 | "body": { 841 | "mode": "raw", 842 | "raw": "{\n \"user\": \"default\",\n \"message_id\": NaN,\n \"message\": \"This is the third message\"\n}", 843 | "options": { 844 | "raw": { 845 | "language": "json" 846 | } 847 | } 848 | }, 849 | "url": { 850 | "raw": "localhost:8000/notaduck/messages/", 851 | "host": [ 852 | "localhost" 853 | ], 854 | "port": "8000", 855 | "path": [ 856 | "notaduck", 857 | "messages", 858 | "" 859 | ] 860 | } 861 | }, 862 | "response": [] 863 | } 864 | ] 865 | }, 866 | { 867 | "name": "Bid", 868 | "item": [ 869 | { 870 | "name": "localhost:8000/notaduck/bid/", 871 | "request": { 872 | "method": "POST", 873 | "header": [], 874 | "body": { 875 | "mode": "raw", 876 | "raw": "{\n \"user\": \"Hacker\",\n \"bid\": NaN\n}", 877 | "options": { 878 | "raw": { 879 | "language": "json" 880 | } 881 | } 882 | }, 883 | "url": { 884 | "raw": "localhost:8000/notaduck/bid/", 885 | "host": [ 886 | "localhost" 887 | ], 888 | "port": "8000", 889 | "path": [ 890 | "notaduck", 891 | "bid", 892 | "" 893 | ] 894 | } 895 | }, 896 | "response": [] 897 | } 898 | ] 899 | } 900 | ] 901 | } -------------------------------------------------------------------------------- /woohoo/README.md: -------------------------------------------------------------------------------- 1 | # Woohoo! 2 | 3 | This django application illustrates some of the failure modes described by the presentation. 4 | 5 | The postman collection provides additional runnable documentation. 6 | 7 | ## Running 8 | 9 | ### Docker 10 | ``` 11 | cd woohoo 12 | docker build -t notaduck . 13 | docker run -it -p 8000:8000 notaduck 14 | ``` 15 | 16 | ### Django 17 | If you already have django installed you can do the typical 18 | `python manage.py runserver` 19 | 20 | 21 | ## Examples 22 | All the action happens here: `/woohoo/notaduck/views.py` 23 | 24 | ### /notaduck/ 25 | `curl -X POST localhost:8000/notaduck/?vara=1&varb=1 -d '{"foo":123,"q":123}'` 26 | The base URL loads two query parameters, and two json body attributes and then applies various casts and math operations, it outputs the results (and types) so that you can get a feel for how NaN and INFs are handled. 27 | 28 | ### /notaduck/rate 29 | 30 | Rate allows users to submit a rating for a book, users can only submit one review per title. Rate keeps track of each book's average rating and displays the name of the highest rated book. Here an attacker can submit a malicious rating for books they do not like, causing their average to be effectively zero. 31 | 32 | ### /notaduck/blindbids 33 | 34 | Allows users to submit bids for a blind auction. Upon request of `GET /notaduck/whowon` the results of the auction will be tallied using sorting mechanism. An attacker can submit malicious bids that interfere with the sorting and cause their own low bid to win, dispite higher bids being in the system. 35 | 36 | ### /notaduck/bid 37 | 38 | Another bidding case, however in this case the mechanism only allows the attacker to achieve being selected the winner, their bid will be nan. 39 | -------------------------------------------------------------------------------- /woohoo/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/db.sqlite3 -------------------------------------------------------------------------------- /woohoo/ducktales/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/ducktales/__init__.py -------------------------------------------------------------------------------- /woohoo/ducktales/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/ducktales/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /woohoo/ducktales/__pycache__/settings.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/ducktales/__pycache__/settings.cpython-39.pyc -------------------------------------------------------------------------------- /woohoo/ducktales/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/ducktales/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /woohoo/ducktales/__pycache__/wsgi.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/ducktales/__pycache__/wsgi.cpython-39.pyc -------------------------------------------------------------------------------- /woohoo/ducktales/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for ducktales project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ducktales.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /woohoo/ducktales/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for ducktales project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-o%x_&nc540*3m0^kqqqd10(2h_m31=ny86o$$+d44s+w33%z1w' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['localhost'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'ducktales.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'ducktales.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': BASE_DIR / 'db.sqlite3', 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | 122 | # Default primary key field type 123 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 124 | 125 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 126 | -------------------------------------------------------------------------------- /woohoo/ducktales/urls.py: -------------------------------------------------------------------------------- 1 | """ducktales URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.contrib import admin 18 | from django.urls import include, path 19 | 20 | urlpatterns = [ 21 | path('notaduck/', include('notaduck.urls')), 22 | path('admin/', admin.site.urls), 23 | ] 24 | -------------------------------------------------------------------------------- /woohoo/ducktales/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for ducktales project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ducktales.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /woohoo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ducktales.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /woohoo/notaduck/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/notaduck/__init__.py -------------------------------------------------------------------------------- /woohoo/notaduck/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/notaduck/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /woohoo/notaduck/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/notaduck/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /woohoo/notaduck/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/notaduck/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /woohoo/notaduck/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /woohoo/notaduck/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NotaduckConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'notaduck' 7 | -------------------------------------------------------------------------------- /woohoo/notaduck/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProZachJ/ducktales/c599e468a905032ccc93ec968e10aef460320c85/woohoo/notaduck/migrations/__init__.py -------------------------------------------------------------------------------- /woohoo/notaduck/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /woohoo/notaduck/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /woohoo/notaduck/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | path('messages/', views.messages, name='messages'), 8 | path('bid/', views.bids, name='bids'), 9 | path('rate/', views.ratings, name='ratings'), 10 | path('blindbids/', views.blindbids, name='blindbids'), 11 | path('whowon/', views.whowon, name='whowon'), 12 | ] 13 | -------------------------------------------------------------------------------- /woohoo/notaduck/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.shortcuts import render 3 | from django.http import JsonResponse 4 | from django.http import HttpResponse 5 | from django.views.decorators.csrf import csrf_exempt 6 | 7 | 8 | @csrf_exempt 9 | def index(request): 10 | try: 11 | data = json.loads(request.body) 12 | a = request.GET.get('vara') 13 | a = float(a) 14 | b = int(request.GET.get('varb')) 15 | 16 | resp = {} 17 | for key in data: 18 | math = data[key] / data['q'] 19 | resp[key] = {'type': type( 20 | data[key]).__name__, 'value': data[key], 'math': math, 'math-type': type(math).__name__} 21 | resp['a'] = {'type': type(a).__name__, 'value': a} 22 | resp['b'] = {'type': type(b).__name__, 'value': b} 23 | othervar = a / b 24 | resp['othervar'] = {'type': type(othervar).__name__, 'value': othervar} 25 | except: 26 | print('nope') 27 | return JsonResponse(resp) 28 | 29 | 30 | @csrf_exempt 31 | def messages(request): 32 | try: 33 | newmessage = json.loads(request.body) 34 | old_message = request.session.get('old_message', { 35 | 'user': 'default', 'message_id': 1, 'message': 'This is the first message'}) 36 | if old_message['message_id'] == newmessage['message_id']: 37 | return JsonResponse({'Error': 'Duplicate message id'}) 38 | else: 39 | old_message = newmessage 40 | request.session['old_message'] = old_message 41 | except: 42 | print("Something went wrong") 43 | return JsonResponse(old_message) 44 | 45 | 46 | @csrf_exempt 47 | def bids(request): 48 | try: 49 | bid = json.loads(request.body) 50 | current_high_bid = request.session.get( 51 | 'high_bid', {'user': 'starting bid', 'bid': 0.00}) 52 | if bid['bid'] < current_high_bid['bid']: 53 | return JsonResponse({'Message': 'Your bid is too low'}) 54 | else: 55 | current_high_bid = bid 56 | request.session['high_bid'] = current_high_bid 57 | except: 58 | print("Something went wrong") 59 | return JsonResponse({"High Bid": current_high_bid}) 60 | 61 | 62 | @csrf_exempt 63 | def ratings(request): 64 | total_ratings = {} 65 | try: 66 | newrating = json.loads(request.body) 67 | currentratings = request.session.get('ratings', {}) 68 | if newrating['book'] in currentratings: 69 | for rating in currentratings[newrating['book']]: 70 | if rating['user'] == newrating['user']: 71 | msg = 'You have already submitted a review for ' + \ 72 | newrating['book'] 73 | return JsonResponse({'Error': msg}) 74 | currentratings[newrating['book']].append({ 75 | 'rating': newrating['rating'], 'user': newrating['user']}) 76 | request.session['ratings'] = currentratings 77 | else: 78 | currentratings[newrating['book']] = [{ 79 | 'rating': newrating['rating'], 'user': newrating['user']}] 80 | request.session['ratings'] = currentratings 81 | for book in currentratings: 82 | for rating in currentratings[book]: 83 | if book in total_ratings: 84 | total_ratings[book] = total_ratings[book] + \ 85 | rating['rating'] 86 | else: 87 | total_ratings[book] = rating['rating'] 88 | total_ratings[book] = total_ratings[book] / \ 89 | len(currentratings[book]) 90 | winning_score = 0 91 | winner = {} 92 | for score in total_ratings: 93 | if total_ratings[score] > winning_score: 94 | winning_score = total_ratings[score] 95 | winner = score 96 | total_ratings['highest_rated'] = winner 97 | except: 98 | print("Something went wrong") 99 | return JsonResponse(total_ratings) 100 | 101 | 102 | @csrf_exempt 103 | def blindbids(request): 104 | try: 105 | bid = json.loads(request.body) 106 | bids = request.session.get('bids', []) 107 | bid_order = request.session.get('bid_order', {}) 108 | bid_num = len(bids) + 1 109 | bids.append(bid['bid']) 110 | bid_order[bid_num] = bid['user'] 111 | request.session['bids'] = bids 112 | request.session['bid_order'] = bid_order 113 | print(bids) 114 | except: 115 | print("Something went wrong") 116 | response = bid['user'] + \ 117 | " thank you for your bid of $" + str(bid['bid']) 118 | return JsonResponse({"mesage": response}) 119 | 120 | 121 | def whowon(request): 122 | response = '' 123 | try: 124 | bids = request.session.get('bids', []) 125 | bid_order = request.session.get('bid_order', {}) 126 | print(bids) 127 | print(bid_order) 128 | sorted_bids = sorted(bids) 129 | largest_bid = sorted_bids[len(sorted_bids) - 1] 130 | temp = 0 131 | for bid in bids: 132 | temp = temp + 1 133 | if largest_bid == bid: 134 | response = bid_order[str(temp)] + \ 135 | " has the winning bid! of $" + str(bid) 136 | except: 137 | print("Something went wrong") 138 | return JsonResponse({"winner": response}) 139 | -------------------------------------------------------------------------------- /woohoo/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.4.1 2 | Django==3.2.9 3 | har2postman==0.1.3 4 | jsonpath==0.82 5 | protobuf==3.17.3 6 | pytz==2021.3 7 | six==1.16.0 8 | sqlparse==0.4.2 9 | --------------------------------------------------------------------------------