diff --git a/lib/parsers.c b/lib/parsers.c index 879db995..cfacaced 100644 --- a/lib/parsers.c +++ b/lib/parsers.c @@ -586,6 +586,23 @@ lws_parse(struct lws *wsi, unsigned char c) if (issue_char(wsi, '/') < 0) return -1; + if (wsi->u.hdr.ups == URIPS_SEEN_SLASH_DOT_DOT) { + /* + * back up one dir level if possible + * safe against header fragmentation because + * the method URI can only be in 1 fragment + */ + if (ah->frags[ah->nfrag].len > 2) { + ah->pos--; + ah->frags[ah->nfrag].len--; + do { + ah->pos--; + ah->frags[ah->nfrag].len--; + } while (ah->frags[ah->nfrag].len > 1 && + ah->data[ah->pos] != '/'); + } + } + /* begin parsing HTTP version: */ if (issue_char(wsi, '\0') < 0) return -1; @@ -593,7 +610,10 @@ lws_parse(struct lws *wsi, unsigned char c) goto start_fragment; } - /* special URI processing... convert %xx */ + /* + * PRIORITY 1 + * special URI processing... convert %xx + */ switch (wsi->u.hdr.ues) { case URIES_IDLE: @@ -603,30 +623,19 @@ lws_parse(struct lws *wsi, unsigned char c) } break; case URIES_SEEN_PERCENT: - if (char_to_hex(c) < 0) { - /* regurgitate */ - if (issue_char(wsi, '%') < 0) - return -1; - wsi->u.hdr.ues = URIES_IDLE; - /* continue on to assess c */ - break; - } + if (char_to_hex(c) < 0) + /* illegal post-% char */ + goto forbid; + wsi->u.hdr.esc_stash = c; wsi->u.hdr.ues = URIES_SEEN_PERCENT_H1; goto swallow; case URIES_SEEN_PERCENT_H1: - if (char_to_hex(c) < 0) { - /* regurgitate */ - if (issue_char(wsi, '%') < 0) - return -1; - wsi->u.hdr.ues = URIES_IDLE; - /* regurgitate + assess */ - if (lws_parse(wsi, wsi->u.hdr.esc_stash) < 0) - return -1; - /* continue on to assess c */ - break; - } + if (char_to_hex(c) < 0) + /* illegal post-% char */ + goto forbid; + c = (char_to_hex(wsi->u.hdr.esc_stash) << 4) | char_to_hex(c); enc = 1; @@ -635,6 +644,7 @@ lws_parse(struct lws *wsi, unsigned char c) } /* + * PRIORITY 2 * special URI processing... * convert /.. or /... or /../ etc to / * convert /./ to / @@ -693,20 +703,6 @@ lws_parse(struct lws *wsi, unsigned char c) case URIPS_SEEN_SLASH_DOT: /* swallow second . */ if (c == '.') { - /* - * back up one dir level if possible - * safe against header fragmentation because - * the method URI can only be in 1 fragment - */ - if (ah->frags[ah->nfrag].len > 2) { - ah->pos--; - ah->frags[ah->nfrag].len--; - do { - ah->pos--; - ah->frags[ah->nfrag].len--; - } while (ah->frags[ah->nfrag].len > 1 && - ah->data[ah->pos] != '/'); - } wsi->u.hdr.ups = URIPS_SEEN_SLASH_DOT_DOT; goto swallow; } @@ -722,19 +718,44 @@ lws_parse(struct lws *wsi, unsigned char c) break; case URIPS_SEEN_SLASH_DOT_DOT: - /* swallow prior .. chars and any subsequent . */ - if (c == '.') + + /* /../ or /..[End of URI] --> backup to last / */ + if (c == '/' || c == '?') { + /* + * back up one dir level if possible + * safe against header fragmentation because + * the method URI can only be in 1 fragment + */ + if (ah->frags[ah->nfrag].len > 2) { + ah->pos--; + ah->frags[ah->nfrag].len--; + do { + ah->pos--; + ah->frags[ah->nfrag].len--; + } while (ah->frags[ah->nfrag].len > 1 && + ah->data[ah->pos] != '/'); + } + wsi->u.hdr.ups = URIPS_SEEN_SLASH; + if (ah->frags[ah->nfrag].len > 1) + break; goto swallow; - /* last issued was /, so another / == // */ - if (c == '/') - goto swallow; - /* last we issued was / so SEEN_SLASH */ - wsi->u.hdr.ups = URIPS_SEEN_SLASH; + } + + /* /..[^/] ... regurgitate and allow */ + + if (issue_char(wsi, '.') < 0) + return -1; + if (issue_char(wsi, '.') < 0) + return -1; + wsi->u.hdr.ups = URIPS_IDLE; break; } if (c == '?' && !enc && !ah->frag_index[WSI_TOKEN_HTTP_URI_ARGS]) { /* start of URI arguments */ + if (wsi->u.hdr.ues != URIES_IDLE) + goto forbid; + /* seal off uri header */ if (issue_char(wsi, '\0') < 0) return -1; @@ -754,10 +775,12 @@ lws_parse(struct lws *wsi, unsigned char c) } check_eol: - /* bail at EOL */ if (wsi->u.hdr.parser_state != WSI_TOKEN_CHALLENGE && c == '\x0d') { + if (wsi->u.hdr.ues != URIES_IDLE) + goto forbid; + c = '\0'; wsi->u.hdr.parser_state = WSI_TOKEN_SKIPPING_SAW_CR; lwsl_parser("*\n"); @@ -803,7 +826,7 @@ swallow: */ if (m == ARRAY_SIZE(methods)) { lwsl_info("Unknown method - dropping\n"); - return -1; + goto forbid; } break; } @@ -891,6 +914,8 @@ excessive: case WSI_TOKEN_SKIPPING_SAW_CR: lwsl_parser("WSI_TOKEN_SKIPPING_SAW_CR '%c'\n", c); + if (wsi->u.hdr.ues != URIES_IDLE) + goto forbid; if (c == '\x0a') { wsi->u.hdr.parser_state = WSI_TOKEN_NAME_PART; wsi->u.hdr.lextable_pos = 0; @@ -907,7 +932,8 @@ excessive: return 0; set_parsing_complete: - + if (wsi->u.hdr.ues != URIES_IDLE) + goto forbid; if (lws_hdr_total_length(wsi, WSI_TOKEN_UPGRADE)) { if (lws_hdr_total_length(wsi, WSI_TOKEN_VERSION)) wsi->ietf_spec_revision = @@ -919,6 +945,11 @@ set_parsing_complete: wsi->hdr_parsing_completed = 1; return 0; + +forbid: + lwsl_notice(" forbidding on uri sanitation\n"); + lws_return_http_status(wsi, HTTP_STATUS_FORBIDDEN, NULL); + return -1; } diff --git a/lib/server.c b/lib/server.c index 3103256e..5a3ec799 100644 --- a/lib/server.c +++ b/lib/server.c @@ -376,7 +376,7 @@ lws_http_action(struct lws *wsi) goto bail_nuke_ah; if (lws_add_http_header_status(wsi, 301, &p, end)) goto bail_nuke_ah; - n = sprintf((char *)end, "htt struct lws_http_mount *hm;ps://%s/", + n = sprintf((char *)end, "https://%s/", lws_hdr_simple_ptr(wsi, WSI_TOKEN_HOST)); if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, end, n, &p, end)) diff --git a/test-server/attack.sh b/test-server/attack.sh index f1a4e1bf..7fe3d841 100755 --- a/test-server/attack.sh +++ b/test-server/attack.sh @@ -35,6 +35,14 @@ function check { fi fi + if [ "$1" = "rejected" ] ; then + if [ -z "`grep '

406

' /tmp/lwscap`" ] ; then + echo "FAIL: should have told forbidden (test server has no dirs)" + exit 1 + fi + fi + + if [ "$1" = "media" ] ; then if [ -z "`grep '

415

' /tmp/lwscap`" ] ; then echo "FAIL: should have told unknown media type" @@ -73,7 +81,7 @@ function check { rm -rf $LOG killall libwebsockets-test-server 2>/dev/null -libwebsockets-test-server -d31 2>> $LOG & +libwebsockets-test-server -d15 2>> $LOG & CPID=$! while [ -z "`grep Listening $LOG`" ] ; do @@ -233,7 +241,7 @@ echo echo "---- directory attack 1 (/../../../../etc/passwd should be /etc/passswd)" rm -f /tmp/lwscap echo -e "GET /../../../../etc/passwd HTTP/1.1\x0d\x0a\x0d\x0a" | nc $SERVER $PORT | sed '1,/^\r$/d'> /tmp/lwscap -check forbidden +check rejected check echo @@ -275,14 +283,14 @@ echo echo "---- directory attack 7 (/%2e%2e%2f../../../etc/passwd should be /etc/passswd)" rm -f /tmp/lwscap echo -e "GET /%2e%2e%2f../../../etc/passwd HTTP/1.1\x0d\x0a\x0d\x0a" | nc $SERVER $PORT | sed '1,/^\r$/d'> /tmp/lwscap -check forbidden +check rejected check echo echo "---- directory attack 8 (%2f%2e%2e%2f%2e./.%2e/.%2e%2fetc/passwd should be /etc/passswd)" rm -f /tmp/lwscap echo -e "GET %2f%2e%2e%2f%2e./.%2e/.%2e%2fetc/passwd HTTP/1.1\x0d\x0a\x0d\x0a" | nc $SERVER $PORT | sed '1,/^\r$/d'> /tmp/lwscap -check forbidden +check rejected check echo @@ -295,6 +303,449 @@ if [ "$good" != "`md5sum /tmp/lwsdump | cut -d' ' -f 1`" ] ; then exit 1 fi +echo +echo "---- mass testing uri variations" + +rm -f /tmp/results + +for i in \ +/..../ \ +/.../. \ +/...// \ +/.../a \ +/.../w \ +/.../? \ +/.../% \ +/../.. \ +/.././ \ +/../.a \ +/../.w \ +/../.. \ +/../.% \ +/..//. \ +/../// \ +/..//a \ +/..//w \ +/..//? \ +/..//% \ +/../a. \ +/../a/ \ +/../aa \ +/../aw \ +/../a? \ +/../a% \ +/../w. \ +/../w/ \ +/../wa \ +/../ww \ +/../w? \ +/../w% \ +/../?. \ +/../?/ \ +/../?a \ +/../?w \ +/../?? \ +/../?% \ +/../%. \ +/../%/ \ +/../%a \ +/../%w \ +/../%? \ +/../%% \ +/./... \ +/./../ \ +/./..a \ +/./..w \ +/./..? \ +/./..% \ +/.//.. \ +/.a../ \ +/.a/.. \ +/.w../ \ +/.w/.. \ +/.?../ \ +/../.. \ +/.%../ \ +/.%/.. \ +//.... \ +//.../ \ +//...a \ +//...w \ +//...? \ +//...% \ +//../. \ +//..// \ +//../a \ +//../w \ +//../? \ +//../% \ +//..a. \ +//..a/ \ +//..aa \ +//..aw \ +//..a? \ +//..a% \ +//..w. \ +//..w/ \ +//..wa \ +//..ww \ +//..w? \ +//..w% \ +//..?. \ +//..?/ \ +//..?a \ +//..?w \ +//..?? \ +//..?% \ +//..%. \ +//..%/ \ +//..%a \ +//..%w \ +//..%? \ +//..%% \ +//./.. \ +///... \ +///../ \ +///..a \ +///..w \ +///..? \ +///..% \ +////.. \ +//a../ \ +//a/.. \ +//w../ \ +//w/.. \ +//?../ \ +//?/.. \ +//%../ \ +//%/.. \ +/a.../ \ +/a../. \ +/a..// \ +/a../a \ +/a../w \ +/a../? \ +/a../% \ +/a./.. \ +/a/... \ +/a/../ \ +/a/..a \ +/a/..w \ +/a/..? \ +/a/..% \ +/a//.. \ +/aa../ \ +/aa/.. \ +/aw../ \ +/aw/.. \ +/a?../ \ +/a?/.. \ +/a%../ \ +/a%/.. \ +/w.../ \ +/w../. \ +/w..// \ +/w../a \ +/w../w \ +/w../? \ +/w../% \ +/w./.. \ +/w/... \ +/w/../ \ +/w/..a \ +/w/..w \ +/w/..? \ +/w/..% \ +/w//.. \ +/wa../ \ +/wa/.. \ +/ww../ \ +/ww/.. \ +/w?../ \ +/w?/.. \ +/w%../ \ +/w%/.. \ +/?.../ \ +/?../. \ +/?..// \ +/?../a \ +/?../w \ +/?../? \ +/?../% \ +/?./.. \ +/?/... \ +/?/../ \ +/?/..a \ +/?/..w \ +/?/..? \ +/?/..% \ +/?//.. \ +/?a../ \ +/?a/.. \ +/?w../ \ +/?w/.. \ +/??../ \ +/??/.. \ +/?%../ \ +/?%/.. \ +/%.../ \ +/%../. \ +/%..// \ +/%../a \ +/%../w \ +/%../? \ +/%../% \ +/%./.. \ +/%/... \ +/%/../ \ +/%/..a \ +/%/..w \ +/%/..? \ +/%/..% \ +/%//.. \ +/%a../ \ +/%a/.. \ +/%w../ \ +/%w/.. \ +/%?../ \ +/%?/.. \ +/%%../ \ +/%%/.. \ +/a/w/../a \ +/path/to/dir/../other/dir \ +; do + +R=`rm -f /tmp/lwscap ; echo -n -e "GET $i HTTP/1.0\r\n\r\n" | nc localhost 7681 2>/dev/null >/tmp/lwscap; head -n1 /tmp/lwscap| cut -d' ' -f2` + +cat /tmp/lwscap | head -n1 +echo ==== $R + + +if [ "$R" != "403" ]; then + U=`cat $LOG | grep lws_http_serve | tail -n 1 | cut -d':' -f3 | cut -d' ' -f2` + echo $U + echo "- \"$i\" -> $R \"$U\"" >>/tmp/results +else + echo "- \"$i\" -> $R" >>/tmp/results +fi +done + +cat </tmp/lwsresult1 +- "/..../" -> 406 "/..../" +- "/.../." -> 406 "/.../" +- "/...//" -> 406 "/.../" +- "/.../a" -> 406 "/.../a" +- "/.../w" -> 406 "/.../w" +- "/.../?" -> 406 "/.../" +- "/.../%" -> 403 +- "/../.." -> 200 "/" +- "/.././" -> 200 "/" +- "/../.a" -> 415 "/.a" +- "/../.w" -> 415 "/.w" +- "/../.." -> 200 "/" +- "/../.%" -> 403 +- "/..//." -> 200 "/" +- "/..///" -> 200 "/" +- "/..//a" -> 415 "/a" +- "/..//w" -> 415 "/w" +- "/..//?" -> 200 "/" +- "/..//%" -> 403 +- "/../a." -> 415 "/a." +- "/../a/" -> 406 "/a/" +- "/../aa" -> 415 "/aa" +- "/../aw" -> 415 "/aw" +- "/../a?" -> 415 "/a" +- "/../a%" -> 403 +- "/../w." -> 415 "/w." +- "/../w/" -> 406 "/w/" +- "/../wa" -> 415 "/wa" +- "/../ww" -> 415 "/ww" +- "/../w?" -> 415 "/w" +- "/../w%" -> 403 +- "/../?." -> 200 "/" +- "/../?/" -> 200 "/" +- "/../?a" -> 200 "/" +- "/../?w" -> 200 "/" +- "/../??" -> 200 "/" +- "/../?%" -> 403 +- "/../%." -> 403 +- "/../%/" -> 403 +- "/../%a" -> 403 +- "/../%w" -> 403 +- "/../%?" -> 403 +- "/../%%" -> 403 +- "/./..." -> 415 "/..." +- "/./../" -> 200 "/" +- "/./..a" -> 415 "/..a" +- "/./..w" -> 415 "/..w" +- "/./..?" -> 200 "/" +- "/./..%" -> 403 +- "/.//.." -> 200 "/" +- "/.a../" -> 406 "/.a../" +- "/.a/.." -> 200 "/" +- "/.w../" -> 406 "/.w../" +- "/.w/.." -> 200 "/" +- "/.?../" -> 415 "/." +- "/../.." -> 200 "/" +- "/.%../" -> 403 +- "/.%/.." -> 403 +- "//...." -> 415 "/...." +- "//.../" -> 406 "/.../" +- "//...a" -> 415 "/...a" +- "//...w" -> 415 "/...w" +- "//...?" -> 415 "/..." +- "//...%" -> 403 +- "//../." -> 200 "/" +- "//..//" -> 200 "/" +- "//../a" -> 415 "/a" +- "//../w" -> 415 "/w" +- "//../?" -> 200 "/" +- "//../%" -> 403 +- "//..a." -> 415 "/..a." +- "//..a/" -> 406 "/..a/" +- "//..aa" -> 415 "/..aa" +- "//..aw" -> 415 "/..aw" +- "//..a?" -> 415 "/..a" +- "//..a%" -> 403 +- "//..w." -> 415 "/..w." +- "//..w/" -> 406 "/..w/" +- "//..wa" -> 415 "/..wa" +- "//..ww" -> 415 "/..ww" +- "//..w?" -> 415 "/..w" +- "//..w%" -> 403 +- "//..?." -> 200 "/" +- "//..?/" -> 200 "/" +- "//..?a" -> 415 "/a" +- "//..?w" -> 415 "/w" +- "//..??" -> 200 "/" +- "//..?%" -> 403 +- "//..%." -> 403 +- "//..%/" -> 403 +- "//..%a" -> 403 +- "//..%w" -> 403 +- "//..%?" -> 403 +- "//..%%" -> 403 +- "//./.." -> 200 "/" +- "///..." -> 415 "/..." +- "///../" -> 200 "/" +- "///..a" -> 415 "/..a" +- "///..w" -> 415 "/..w" +- "///..?" -> 200 "/" +- "///..%" -> 403 +- "////.." -> 200 "/" +- "//a../" -> 406 "/a../" +- "//a/.." -> 200 "/" +- "//w../" -> 406 "/w../" +- "//w/.." -> 200 "/" +- "//?../" -> 200 "/" +- "//?/.." -> 200 "/" +- "//%../" -> 403 +- "//%/.." -> 403 +- "/a.../" -> 406 "/a.../" +- "/a../." -> 406 "/a../" +- "/a..//" -> 406 "/a../" +- "/a../a" -> 406 "/a../a" +- "/a../w" -> 406 "/a../w" +- "/a../?" -> 406 "/a../" +- "/a../%" -> 403 +- "/a./.." -> 200 "/" +- "/a/..." -> 406 "/a/..." +- "/a/../" -> 200 "/" +- "/a/..a" -> 406 "/a/..a" +- "/a/..w" -> 406 "/a/..w" +- "/a/..?" -> 200 "/" +- "/a/..%" -> 403 +- "/a//.." -> 200 "/" +- "/aa../" -> 406 "/aa../" +- "/aa/.." -> 200 "/" +- "/aw../" -> 406 "/aw../" +- "/aw/.." -> 200 "/" +- "/a?../" -> 415 "/a" +- "/a?/.." -> 415 "/a" +- "/a%../" -> 403 +- "/a%/.." -> 403 +- "/w.../" -> 406 "/w.../" +- "/w../." -> 406 "/w../" +- "/w..//" -> 406 "/w../" +- "/w../a" -> 406 "/w../a" +- "/w../w" -> 406 "/w../w" +- "/w../?" -> 406 "/w../" +- "/w../%" -> 403 +- "/w./.." -> 200 "/" +- "/w/..." -> 406 "/w/..." +- "/w/../" -> 200 "/" +- "/w/..a" -> 406 "/w/..a" +- "/w/..w" -> 406 "/w/..w" +- "/w/..?" -> 200 "/" +- "/w/..%" -> 403 +- "/w//.." -> 200 "/" +- "/wa../" -> 406 "/wa../" +- "/wa/.." -> 200 "/" +- "/ww../" -> 406 "/ww../" +- "/ww/.." -> 200 "/" +- "/w?../" -> 415 "/w" +- "/w?/.." -> 415 "/w" +- "/w%../" -> 403 +- "/w%/.." -> 403 +- "/?.../" -> 200 "/" +- "/?../." -> 200 "/" +- "/?..//" -> 200 "/" +- "/?../a" -> 200 "/" +- "/?../w" -> 200 "/" +- "/?../?" -> 200 "/" +- "/?../%" -> 403 +- "/?./.." -> 200 "/" +- "/?/..." -> 200 "/" +- "/?/../" -> 200 "/" +- "/?/..a" -> 200 "/" +- "/?/..w" -> 200 "/" +- "/?/..?" -> 200 "/" +- "/?/..%" -> 403 +- "/?//.." -> 200 "/" +- "/?a../" -> 200 "/" +- "/?a/.." -> 200 "/" +- "/?w../" -> 200 "/" +- "/?w/.." -> 200 "/" +- "/??../" -> 200 "/" +- "/??/.." -> 200 "/" +- "/?%../" -> 403 +- "/?%/.." -> 403 +- "/%.../" -> 403 +- "/%../." -> 403 +- "/%..//" -> 403 +- "/%../a" -> 403 +- "/%../w" -> 403 +- "/%../?" -> 403 +- "/%../%" -> 403 +- "/%./.." -> 403 +- "/%/..." -> 403 +- "/%/../" -> 403 +- "/%/..a" -> 403 +- "/%/..w" -> 403 +- "/%/..?" -> 403 +- "/%/..%" -> 403 +- "/%//.." -> 403 +- "/%a../" -> 403 +- "/%a/.." -> 403 +- "/%w../" -> 403 +- "/%w/.." -> 403 +- "/%?../" -> 403 +- "/%?/.." -> 403 +- "/%%../" -> 403 +- "/%%/.." -> 403 +- "/a/w/../a" -> 406 "/a/a" +- "/path/to/dir/../other/dir" -> 406 "/path/to/other/dir" +EOF + +if [ "`md5sum /tmp/results | cut -d' ' -f 1`" != "`md5sum /tmp/lwsresult1 | cut -d' ' -f1`" ] ; then + echo "Differences..." + diff -urN /tmp/results /tmp/lwsresult1 + exit 1 +else + echo "OK" +fi + + echo echo "--- survived OK ---" kill -2 $CPID diff --git a/test-server/test-server-http.c b/test-server/test-server-http.c index a142b701..902e9456 100644 --- a/test-server/test-server-http.c +++ b/test-server/test-server-http.c @@ -140,9 +140,12 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, struct lws_pollargs *pa = (struct lws_pollargs *)in; #endif + switch (reason) { case LWS_CALLBACK_HTTP: + lwsl_notice("lws_http_serve: %s\n",in); + if (debug_level & LLL_INFO) { dump_handshake_info(wsi); @@ -207,7 +210,7 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, #if 1 /* this example server has no concept of directories */ if (strchr((const char *)in + 1, '/')) { - lws_return_http_status(wsi, HTTP_STATUS_FORBIDDEN, NULL); + lws_return_http_status(wsi, HTTP_STATUS_NOT_ACCEPTABLE, NULL); goto try_to_reuse; } #endif