simplefiles

A lightweight C CGI web application for managing files.
git clone https://scm.kuandu.systems/git-raw/simplefiles.git
Log | Files | Refs | README | LICENSE

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