delete.sh (13163B)
1 #!/bin/sh 2 3 set -eu 4 5 . "$(dirname "$0")/testlib.sh" 6 7 CGI=${1:-build/delete.cgi} 8 9 delete() { 10 env \ 11 SCRIPT_NAME=/default/delete.cgi \ 12 STORAGE_ROOT="$TESTROOT/storage" \ 13 SIMPLEFILES_ROOT=./html \ 14 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 15 REQUEST_METHOD=GET \ 16 QUERY_STRING="${1:-}" \ 17 REMOTE_USER=bob \ 18 "$CGI" 19 } 20 21 delete_user() { 22 user=$1 23 query=${2:-} 24 25 env \ 26 SCRIPT_NAME=/default/delete.cgi \ 27 STORAGE_ROOT="$TESTROOT/storage" \ 28 SIMPLEFILES_ROOT=./html \ 29 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 30 REQUEST_METHOD=GET \ 31 QUERY_STRING="$query" \ 32 REMOTE_USER="$user" \ 33 "$CGI" 34 } 35 36 delete_no_user() { 37 query=${1:-} 38 39 ( 40 unset REMOTE_USER 41 42 export SCRIPT_NAME=/default/delete.cgi 43 export STORAGE_ROOT="$TESTROOT/storage" 44 export SIMPLEFILES_ROOT=./html 45 export SIMPLEFILES_CONFIG="$TEST_CONFIG" 46 export REQUEST_METHOD=GET 47 export QUERY_STRING="$query" 48 49 "$CGI" 50 ) 51 } 52 53 delete_post() { 54 body=${1:-} 55 56 printf '%s' "$body" | 57 env \ 58 SCRIPT_NAME=/default/delete.cgi \ 59 STORAGE_ROOT="$TESTROOT/storage" \ 60 REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \ 61 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 62 SIMPLEFILES_ROOT=./html \ 63 HTTP_ORIGIN="$TEST_CSRF_ORIGIN" \ 64 REMOTE_USER=bob \ 65 REQUEST_METHOD=POST \ 66 CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \ 67 "$CGI" 68 } 69 70 delete_post_user() { 71 user=$1 72 body=${2:-} 73 74 printf '%s' "$body" | 75 env \ 76 SCRIPT_NAME=/default/delete.cgi \ 77 STORAGE_ROOT="$TESTROOT/storage" \ 78 REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \ 79 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 80 SIMPLEFILES_ROOT=./html \ 81 HTTP_ORIGIN="$TEST_CSRF_ORIGIN" \ 82 REMOTE_USER="$user" \ 83 REQUEST_METHOD=POST \ 84 CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \ 85 "$CGI" 86 } 87 88 delete_post_no_user() { 89 body=${1:-} 90 91 printf '%s' "$body" | 92 env \ 93 SCRIPT_NAME=/default/delete.cgi \ 94 STORAGE_ROOT="$TESTROOT/storage" \ 95 REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \ 96 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 97 SIMPLEFILES_ROOT=./html \ 98 HTTP_ORIGIN="$TEST_CSRF_ORIGIN" \ 99 REQUEST_METHOD=POST \ 100 CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \ 101 "$CGI" 102 } 103 104 delete_post_origin() { 105 origin=$1 106 body=${2:-} 107 108 printf '%s' "$body" | 109 env \ 110 SCRIPT_NAME=/default/delete.cgi \ 111 STORAGE_ROOT="$TESTROOT/storage" \ 112 REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \ 113 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 114 SIMPLEFILES_ROOT=./html \ 115 HTTP_ORIGIN="$origin" \ 116 REMOTE_USER=bob \ 117 REQUEST_METHOD=POST \ 118 CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \ 119 "$CGI" 120 } 121 122 delete_post_no_origin() { 123 body=${1:-} 124 125 printf '%s' "$body" | 126 env \ 127 SCRIPT_NAME=/default/delete.cgi \ 128 STORAGE_ROOT="$TESTROOT/storage" \ 129 REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \ 130 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 131 SIMPLEFILES_ROOT=./html \ 132 REMOTE_USER=bob \ 133 REQUEST_METHOD=POST \ 134 CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \ 135 "$CGI" 136 } 137 138 #################################################### 139 140 # 141 # Confirmation page 142 # 143 144 reset_testroot 145 146 write_default_repo_config "ADMIN_USER=bob" 147 148 printf 'hello\n' > "$TESTROOT/storage/default/file1.txt" 149 printf 'second\n' > "$TESTROOT/storage/default/file2.txt" 150 151 check_status \ 152 "confirmation page" \ 153 "200 OK" \ 154 delete \ 155 "file=file1.txt" 156 157 check_body \ 158 "filename shown" \ 159 "file1.txt" \ 160 delete \ 161 "file=file1.txt" 162 163 check_body \ 164 "delete form" \ 165 '<form method="post"' \ 166 delete \ 167 "file=file1.txt" 168 169 check_body \ 170 "cancel link" \ 171 "./" \ 172 delete \ 173 "file=file1.txt" 174 175 check_body \ 176 "file parameter with extra query parameters" \ 177 "file2.txt" \ 178 delete \ 179 "foo=bar&file=file2.txt&baz=qux" 180 181 # 182 # Parameter validation 183 # 184 185 check_body \ 186 "missing file parameter" \ 187 "Missing filename" \ 188 delete \ 189 "foo=file1.txt" 190 191 check_body \ 192 "empty file parameter" \ 193 "Invalid filename" \ 194 delete \ 195 "file=" 196 197 check_body \ 198 "missing file" \ 199 "404 Not Found" \ 200 delete \ 201 "file=missing.txt" 202 203 check_body \ 204 "invalid filename" \ 205 "400 Bad Request" \ 206 delete \ 207 "file=../passwd" 208 209 check_body \ 210 "encoded path traversal" \ 211 "400 Bad Request" \ 212 delete \ 213 "file=%2e%2e/passwd" 214 215 check_body \ 216 "mixed encoded traversal" \ 217 "400 Bad Request" \ 218 delete \ 219 "file=.%2e/passwd" 220 221 check_body \ 222 "encoded NUL" \ 223 "400 Bad Request" \ 224 delete \ 225 "file=file%00.txt" 226 227 check_body \ 228 "malformed percent encoding" \ 229 "400 Bad Request" \ 230 delete \ 231 "file=%" 232 233 check_body \ 234 "short percent encoding" \ 235 "400 Bad Request" \ 236 delete \ 237 "file=%2" 238 239 check_body \ 240 "invalid percent encoding" \ 241 "400 Bad Request" \ 242 delete \ 243 "file=%ZZ" 244 245 check_status \ 246 "reject duplicate file parameter" \ 247 "400 Bad Request" \ 248 delete \ 249 "file=file1.txt&file=file2.txt" 250 251 check_body \ 252 "file parameter after another parameter" \ 253 "200 OK" \ 254 delete \ 255 "foo=bar&file=file2.txt" 256 257 check_body \ 258 "file parameter before another parameter" \ 259 "200 OK" \ 260 delete \ 261 "file=file2.txt&foo=bar" 262 263 # 264 # Delete 265 # 266 267 check_body \ 268 "encoded POST path traversal" \ 269 "400 Bad Request" \ 270 delete_post \ 271 "file=%2e%2e/passwd" 272 273 check_body \ 274 "encoded POST NUL" \ 275 "400 Bad Request" \ 276 delete_post \ 277 "file=file%00.txt" 278 279 check_body \ 280 "reject duplicate POST file parameter" \ 281 "400 Bad Request" \ 282 delete_post \ 283 "file=file1.txt&file=file2.txt" 284 285 check_redirect \ 286 "delete redirects" \ 287 "303 See Other" \ 288 "./" \ 289 delete_post \ 290 "file=file1.txt" 291 292 check_file_missing \ 293 "file deleted" \ 294 "$TESTROOT/storage/default/file1.txt" 295 296 check_body \ 297 "delete missing file" \ 298 "File not found" \ 299 delete_post \ 300 "file=file1.txt" 301 302 check_body \ 303 "malformed POST percent encoding" \ 304 "400 Bad Request" \ 305 delete_post \ 306 "file=%" 307 308 check_body \ 309 "invalid POST percent encoding" \ 310 "400 Bad Request" \ 311 delete_post \ 312 "file=%ZZ" 313 314 # 315 # Filename decoding 316 # 317 318 space_file="$TESTROOT/storage/default/hello world.txt" 319 320 printf 'hello' > "$space_file" 321 322 check_redirect \ 323 "plus-decoded delete redirects" \ 324 "303 See Other" \ 325 "./" \ 326 delete_post \ 327 "file=hello+world.txt" 328 329 check_file_missing \ 330 "plus-decoded file deleted" \ 331 "$space_file" 332 333 special_file="$TESTROOT/storage/default/<test>&file.txt" 334 special_body='file=%3Ctest%3E%26file.txt' 335 336 printf 'hello' > "$special_file" 337 338 check_body \ 339 "delete encoded filename" \ 340 "Delete " \ 341 delete \ 342 "$special_body" 343 344 printf 'hello' > "$special_file" 345 346 check_redirect \ 347 "encoded delete redirects" \ 348 "303 See Other" \ 349 "./" \ 350 delete_post \ 351 "$special_body" 352 353 check_file_missing \ 354 "encoded file deleted" \ 355 "$special_file" 356 357 # 358 # Protected file 359 # 360 361 write_default_repo_config "ADMIN_USER=bob" 362 363 check_status \ 364 "reject .config delete" \ 365 "400 Bad Request" \ 366 delete \ 367 "file=.config" 368 369 check_file_exists \ 370 ".config still exists" \ 371 "$TESTROOT/storage/default/.config" 372 373 check_status \ 374 "reject .config POST delete" \ 375 "400 Bad Request" \ 376 delete_post \ 377 "file=.config" 378 379 check_file_exists \ 380 ".config survives POST" \ 381 "$TESTROOT/storage/default/.config" 382 383 # 384 # CSRF 385 # 386 387 reset_testroot 388 389 write_default_repo_config "ADMIN_USER=bob" 390 391 printf 'hello\n' > "$TESTROOT/storage/default/csrf.txt" 392 393 check_status \ 394 "accept configured Origin" \ 395 "303 See Other" \ 396 delete_post_origin \ 397 "http://localhost:8000" \ 398 "file=csrf.txt" 399 400 check_file_missing \ 401 "configured Origin deletes file" \ 402 "$TESTROOT/storage/default/csrf.txt" 403 404 reset_testroot 405 406 write_default_repo_config "ADMIN_USER=bob" 407 408 printf 'hello\n' > "$TESTROOT/storage/default/csrf.txt" 409 410 check_status \ 411 "reject missing Origin" \ 412 "403 Forbidden" \ 413 delete_post_no_origin \ 414 "file=csrf.txt" 415 416 check_file_exists \ 417 "missing Origin preserves file" \ 418 "$TESTROOT/storage/default/csrf.txt" 419 420 reset_testroot 421 422 write_default_repo_config "ADMIN_USER=bob" 423 424 printf 'hello\n' > "$TESTROOT/storage/default/csrf.txt" 425 426 check_status \ 427 "reject wrong Origin" \ 428 "403 Forbidden" \ 429 delete_post_origin \ 430 "https://evil.example" \ 431 "file=csrf.txt" 432 433 check_file_exists \ 434 "wrong Origin preserves file" \ 435 "$TESTROOT/storage/default/csrf.txt" 436 437 check_status \ 438 "reject Origin prefix" \ 439 "403 Forbidden" \ 440 delete_post_origin \ 441 "https://localhost.evil.example" \ 442 "file=csrf.txt" 443 444 check_status \ 445 "reject Origin in path" \ 446 "403 Forbidden" \ 447 delete_post_origin \ 448 "https://evil.example/?origin=https://localhost:8000" \ 449 "file=csrf.txt" 450 451 check_status \ 452 "reject wrong Origin scheme" \ 453 "403 Forbidden" \ 454 delete_post_origin \ 455 "https://localhost:8000" \ 456 "file=csrf.txt" 457 458 reset_testroot 459 460 write_global_config "CSRF_ORIGIN=https://files.example" 461 write_default_repo_config "ADMIN_USER=bob" 462 463 printf 'hello\n' > "$TESTROOT/storage/default/csrf.txt" 464 465 check_status \ 466 "accept custom Origin" \ 467 "303 See Other" \ 468 delete_post_origin \ 469 "https://files.example" \ 470 "file=csrf.txt" 471 472 check_file_missing \ 473 "custom Origin deletes file" \ 474 "$TESTROOT/storage/default/csrf.txt" 475 476 reset_testroot 477 478 write_global_config "CSRF_ORIGIN=https://files.example" 479 write_default_repo_config "ADMIN_USER=bob" 480 481 printf 'hello\n' > "$TESTROOT/storage/default/csrf.txt" 482 483 check_status \ 484 "reject old Origin" \ 485 "403 Forbidden" \ 486 delete_post_origin \ 487 "https://localhost" \ 488 "file=csrf.txt" 489 490 check_file_exists \ 491 "old Origin preserves file" \ 492 "$TESTROOT/storage/default/csrf.txt" 493 494 # 495 # Read-only repository 496 # 497 498 reset_testroot 499 500 write_default_repo_config "READ_ONLY=1" 501 502 printf 'readonly' > "$TESTROOT/storage/default/readonly.txt" 503 504 check_body \ 505 "readonly GET rejected" \ 506 "read-only" \ 507 delete \ 508 "file=readonly.txt" 509 510 check_file_exists \ 511 "readonly GET preserves file" \ 512 "$TESTROOT/storage/default/readonly.txt" 513 514 check_status \ 515 "readonly POST rejected" \ 516 "403 Forbidden" \ 517 delete_post \ 518 "file=readonly.txt" 519 520 check_file_exists \ 521 "readonly POST preserves file" \ 522 "$TESTROOT/storage/default/readonly.txt" 523 524 # 525 # Access control 526 # 527 528 reset_testroot 529 530 write_default_repo_config "" 531 532 printf 'legacy\n' > "$TESTROOT/storage/default/legacy.txt" 533 534 check_status \ 535 "delete rejected without ADMIN_USER" \ 536 "403 Forbidden" \ 537 delete_post \ 538 "file=legacy.txt" 539 540 check_file_exists \ 541 "no ADMIN_USER preserves file" \ 542 "$TESTROOT/storage/default/legacy.txt" 543 544 reset_testroot 545 546 write_default_repo_config "ADMIN_USER=bob" 547 548 printf 'admin\n' > "$TESTROOT/storage/default/admin.txt" 549 550 check_status \ 551 "delete allowed for admin" \ 552 "303 See Other" \ 553 delete_post_user \ 554 "bob" \ 555 "file=admin.txt" 556 557 check_file_missing \ 558 "admin delete succeeded" \ 559 "$TESTROOT/storage/default/admin.txt" 560 561 reset_testroot 562 563 write_default_repo_config "ADMIN_USER=bob" 564 565 printf 'readonly\n' > "$TESTROOT/storage/default/readonly.txt" 566 567 check_status \ 568 "delete rejected for non-admin" \ 569 "403 Forbidden" \ 570 delete_post_user \ 571 "alice" \ 572 "file=readonly.txt" 573 574 check_file_exists \ 575 "non-admin preserves file" \ 576 "$TESTROOT/storage/default/readonly.txt" 577 578 # 579 # GET access control 580 # 581 582 reset_testroot 583 584 write_default_repo_config "ADMIN_USER=bob" 585 586 printf 'hello' > "$TESTROOT/storage/default/file.txt" 587 588 check_status \ 589 "non-admin GET rejected" \ 590 "403 Forbidden" \ 591 delete_user \ 592 "alice" \ 593 "file=file.txt" 594 595 check_file_exists \ 596 "non-admin GET preserves file" \ 597 "$TESTROOT/storage/default/file.txt" 598 599 reset_testroot 600 601 write_default_repo_config "ADMIN_USER=bob" 602 603 printf 'hello' > "$TESTROOT/storage/default/file.txt" 604 605 check_status \ 606 "anonymous GET rejected" \ 607 "403 Forbidden" \ 608 delete_no_user \ 609 "file=file.txt" 610 611 check_file_exists \ 612 "anonymous GET preserves file" \ 613 "$TESTROOT/storage/default/file.txt" 614 615 # 616 # Anonymous read does not grant write access 617 # 618 619 reset_testroot 620 621 write_default_repo_config "ADMIN_USER=bob" 622 write_global_config "ANONYMOUS_READ=1" 623 624 printf 'hello' > "$TESTROOT/storage/default/file.txt" 625 626 check_status \ 627 "anonymous GET rejected despite anonymous read" \ 628 "403 Forbidden" \ 629 delete_no_user \ 630 "file=file.txt" 631 632 check_status \ 633 "anonymous POST rejected despite anonymous read" \ 634 "403 Forbidden" \ 635 delete_post_no_user \ 636 "file=file.txt" 637 638 check_file_exists \ 639 "anonymous source preserved" \ 640 "$TESTROOT/storage/default/file.txt" 641 642 check_file_missing \ 643 "anonymous destination not created" \ 644 "$TESTROOT/storage/default/renamed.txt" 645 646 # 647 # Response generation 648 # 649 650 reset_testroot 651 652 write_default_repo_config "ADMIN_USER=bob" 653 654 printf 'hello\n' > "$TESTROOT/storage/default/file1.txt" 655 656 mkdir -p "$TESTROOT/html" 657 658 printf '{{UNKNOWN_VARIABLE}}' > "$TESTROOT/html/delete.html" 659 660 check_status \ 661 "template failure returns 500" \ 662 "500 Internal Server Error" \ 663 env \ 664 SCRIPT_NAME=/default/delete.cgi \ 665 STORAGE_ROOT="$TESTROOT/storage" \ 666 SIMPLEFILES_ROOT="$TESTROOT/html" \ 667 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 668 REQUEST_METHOD=GET \ 669 QUERY_STRING="file=file1.txt" \ 670 REMOTE_USER=bob \ 671 "$CGI" 672 673 check_not_body \ 674 "template failure does not return 200" \ 675 "Status: 200 OK" \ 676 env \ 677 SCRIPT_NAME=/default/delete.cgi \ 678 STORAGE_ROOT="$TESTROOT/storage" \ 679 SIMPLEFILES_ROOT="$TESTROOT/html" \ 680 SIMPLEFILES_CONFIG="$TEST_CONFIG" \ 681 REQUEST_METHOD=GET \ 682 QUERY_STRING="file=file1.txt" \ 683 REMOTE_USER=bob \ 684 "$CGI" 685 686 test_result