diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3e932e9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + # Opens a PR when libinjection moves, so the differential job runs the port + # against the new C code. + - package-ecosystem: gitsubmodule + directory: / + schedule: + interval: weekly diff --git a/CHANGELOG.md b/CHANGELOG.md index dd33617..eead9f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,8 +45,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). table keeps the behaviour. - A line-coverage gate in CI (`cargo-llvm-cov`), enforcing a floor on the library. +- Dependabot watches the `libinjection-c` submodule, so upstream changes open + a pull request that the differential job runs against. ### Changed +- The C reference is libinjection 4.0.1. The `libinjection-c` submodule had + been pinned at 85e252a (February 2025) since the port began. The port follows + the detection changes since then: + - A number like `1.e` or `10.E` with no exponent digits produces no token, + as MySQL ignores it, so `1' or 1.e(1)` folds and is flagged as in C. + - `LIKE (` is read as a function only when it does not follow an operand + (number, string, bareword, variable or `)`). + - The HTML5 tokenizer reads bytes unsigned when skipping whitespace, so a + 0xFF byte is no longer taken as end of input and attributes after it are + checked. + - The XSS event-handler list follows upstream's 432 entries (145 added, 11 + removed). + - `html_decode_char_at` and the event-handler comparison mirror 4.0.1's + length guards. Verdicts are unchanged. + + 4.0.0 fixed the `htmlencode_startswith` over-read described below; the + harness keeps the zero padding. Over the corpus, now 163,275 inputs, SQLi and + XSS divergences from the C library remain at 0. - The differential fingerprint ceiling is lowered from 1,631 to 0: SQLi fingerprints now match the C library exactly across the corpus. - The FFI harness zero-pads the C input. libinjection reads a few bytes past diff --git a/comparison-bin/tests/differential.rs b/comparison-bin/tests/differential.rs index 410e445..f431fba 100644 --- a/comparison-bin/tests/differential.rs +++ b/comparison-bin/tests/differential.rs @@ -410,15 +410,26 @@ fn nul_in_number_literal_matches_the_c_library() { /// would. The text corpus has no NUL bytes, so only the fuzzer reaches this. #[test] fn nul_as_whitespace_in_html5_matches_the_c_library() { - // A NUL inside an attribute name: C ends the name there, so the trailing - // `"; assert_eq!( libinjectionrs::detect_xss(input).is_injection(), c_xss(input), "NUL-as-whitespace in HTML5 diverges from the C library" ); - assert!(!c_xss(input), "C treats this as safe, and so must the port"); + assert!(c_xss(input), "C flags the onclick after the NUL, and so must the port"); + + // Fuzz-found: the NUL ends the attribute name `2`, the 0xFF after it is a + // second attribute name, and the trailing `"; + let c_is = c_xss(input); + assert_eq!( + libinjectionrs::detect_xss(input).is_injection(), + c_is, + "0xFF before an attribute diverges from the C library" ); + assert!(c_is, "C flags the onfocus handler after 0xFF, and so must the port"); } /// Guards the numeric-entity comparison in `htmlencode_startswith`. C compares @@ -487,3 +507,36 @@ fn numeric_entity_compares_by_its_low_byte_like_the_c_library() { assert!(c_xss(b""), "C flags this XSS, and so must the port"); assert!(!c_xss(b""), "C upcases before truncating, so this is safe"); } + +/// Fuzz-found XSS inputs containing 0xFF bytes. The verdict depends on +/// `h5_skip_white` treating 0xFF as an ordinary byte, so it must match C. +#[test] +fn xss_fuzz_inputs_with_0xff_bytes_match_the_c_library() { + let inputs: [&[u8]; 2] = [ + // crash-472cde1c76cb772c42c53bf83e5bfe071f009983 + &[ + 47, 93, 34, 47, 93, 34, 96, 214, 45, 53, 32, 47, 62, 60, 116, 255, 102, 102, 102, 102, + 39, 96, 10, 39, 10, 90, 127, 60, 112, 10, 120, 96, 170, 84, 40, 47, 60, 39, 61, 255, + 62, 96, 47, 60, 33, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, + 105, 105, 105, 61, 39, 212, 61, 61, 39, 13, 116, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 102, 102, 102, 102, 102, 255, 52, 39, 167, 1, 61, 96, 96, 47, 13, 96, 39, + 45, 53, 32, 47, 62, 60, 116, 255, 102, 102, 102, 102, 102, 91, 102, 96, 102, 102, 102, + 39, 167, 1, 61, 96, 96, 47, 13, + ], + // 8ce9746b + &[ + 60, 112, 60, 112, 10, 47, 96, 2, 34, 96, 47, 13, 60, 47, 13, 13, 13, 96, 47, 84, 111, + 47, 255, 67, 32, 32, 62, 132, 1, 60, 112, 10, 47, 96, 2, 34, 96, 32, 32, 62, 1, 60, + 112, 62, 137, 125, 13, 60, 47, 13, 60, 47, 13, 13, 13, 96, 96, 32, 62, 132, 1, 60, + 112, 10, 47, 96, 2, 34, 96, 32, 34, + ], + ]; + for input in inputs { + let c_is = c_xss(input); + assert_eq!( + libinjectionrs::detect_xss(input).is_injection(), + c_is, + "fuzz input with 0xFF bytes diverges from the C library: {input:?}" + ); + } +} diff --git a/ffi-harness/Makefile b/ffi-harness/Makefile index e6af17b..1edc70f 100644 --- a/ffi-harness/Makefile +++ b/ffi-harness/Makefile @@ -1,5 +1,5 @@ CC = gcc -CFLAGS = -Wall -Wextra -std=c99 -O3 -fno-omit-frame-pointer -g -fPIC -DLIBINJECTION_VERSION=\"3.10.0\" +CFLAGS = -Wall -Wextra -std=c99 -O3 -fno-omit-frame-pointer -g -fPIC INCLUDES = -I../libinjection-c/src SRCDIR = ../libinjection-c/src OBJDIR = obj diff --git a/ffi-harness/README.md b/ffi-harness/README.md index ce78e2f..ec6478c 100644 --- a/ffi-harness/README.md +++ b/ffi-harness/README.md @@ -87,4 +87,3 @@ The Makefile uses: - `-O3` - Optimization level 3 - `-g` - Debug information - `-fno-omit-frame-pointer` - Keep frame pointers for debugging -- `-DLIBINJECTION_VERSION="3.10.0"` - Version definition \ No newline at end of file diff --git a/ffi-harness/harness.c b/ffi-harness/harness.c index 8ba0ad6..f0b7cbd 100644 --- a/ffi-harness/harness.c +++ b/ffi-harness/harness.c @@ -5,13 +5,8 @@ #include #include -// libinjection reads a few bytes past the end of the input on some adversarial -// inputs (an ASan-confirmed over-read in htmlencode_startswith, reached from -// libinjection_is_xss). Past the buffer the bytes are indeterminate, so the -// verdict is not reproducible across builds. Copy the input into a zeroed, -// over-allocated buffer so any such read lands on defined zero bytes and the -// C result is deterministic. The differential then compares against a stable -// C answer rather than stack garbage. +// Copy the input into a zeroed, over-allocated buffer so that any read past +// input_len lands on defined zero bytes and the C verdict is deterministic. #define HARNESS_PAD 16 static char* padded_copy(const char* input, size_t input_len) { diff --git a/libinjection-c b/libinjection-c index 85e252a..bd6fb4e 160000 --- a/libinjection-c +++ b/libinjection-c @@ -1 +1 @@ -Subproject commit 85e252a28981d479822092bad235bb75beb7edfa +Subproject commit bd6fb4e97b433762436d9920c82a16c2b5d44c43 diff --git a/libinjectionrs/src/sqli/mod.rs b/libinjectionrs/src/sqli/mod.rs index b1bdeff..9850062 100644 --- a/libinjectionrs/src/sqli/mod.rs +++ b/libinjectionrs/src/sqli/mod.rs @@ -630,7 +630,13 @@ impl<'a> SqliState<'a> { let val = self.token_vec[left].value_as_str(); self.cstrcasecmp("LIKE", val) == 0 || self.cstrcasecmp("NOT LIKE", val) == 0 } { - if self.token_vec[left + 1].token_type == TokenType::LeftParenthesis { + if self.token_vec[left + 1].token_type == TokenType::LeftParenthesis && + !(left > 0 && + (self.token_vec[left - 1].token_type == TokenType::Number || + self.token_vec[left - 1].token_type == TokenType::String || + self.token_vec[left - 1].token_type == TokenType::Bareword || + self.token_vec[left - 1].token_type == TokenType::Variable || + self.token_vec[left - 1].token_type == TokenType::RightParenthesis)) { // SELECT LIKE(... - it's a function self.token_vec[left].token_type = TokenType::Function; } diff --git a/libinjectionrs/src/sqli/tokenizer.rs b/libinjectionrs/src/sqli/tokenizer.rs index e16ecb8..39ddfdb 100644 --- a/libinjectionrs/src/sqli/tokenizer.rs +++ b/libinjectionrs/src/sqli/tokenizer.rs @@ -1180,11 +1180,9 @@ impl<'a> SqliTokenizer<'a> { } } - // Check for invalid exponential format - if have_e && !have_exp { - let token = &self.input[start_pos..end_pos]; - self.current.assign(TYPE_BAREWORD, start_pos, end_pos - start_pos, token); - } else { + // "1234.e", "10.10E", ".E": MySQL ignores these, so no token is + // assigned and the tokenizer moves past them as it does whitespace. + if !(have_e && !have_exp) { let token = &self.input[start_pos..end_pos]; self.current.assign(TYPE_NUMBER, start_pos, end_pos - start_pos, token); } diff --git a/libinjectionrs/src/xss/blacklists.rs b/libinjectionrs/src/xss/blacklists.rs index 35ec5c7..503fbe2 100644 --- a/libinjectionrs/src/xss/blacklists.rs +++ b/libinjectionrs/src/xss/blacklists.rs @@ -35,206 +35,315 @@ pub const HEX_DECODE_MAP: [i32; 256] = [ // Event handler attributes (on* events) pub const BLACK_ATTR_EVENTS: &[StringType] = &[ StringType { name: "ABORT", atype: AttributeType::Black }, + StringType { name: "ACCESSKEYNOTFOUND", atype: AttributeType::Black }, StringType { name: "ACTIVATE", atype: AttributeType::Black }, StringType { name: "ACTIVE", atype: AttributeType::Black }, StringType { name: "ADDSOURCEBUFFER", atype: AttributeType::Black }, StringType { name: "ADDSTREAM", atype: AttributeType::Black }, StringType { name: "ADDTRACK", atype: AttributeType::Black }, + StringType { name: "AFTERPAINT", atype: AttributeType::Black }, StringType { name: "AFTERPRINT", atype: AttributeType::Black }, + StringType { name: "AFTERSCRIPTEXECUTE", atype: AttributeType::Black }, StringType { name: "ANIMATIONCANCEL", atype: AttributeType::Black }, StringType { name: "ANIMATIONEND", atype: AttributeType::Black }, StringType { name: "ANIMATIONITERATION", atype: AttributeType::Black }, StringType { name: "ANIMATIONSTART", atype: AttributeType::Black }, StringType { name: "AUDIOEND", atype: AttributeType::Black }, + StringType { name: "AUDIOCOMPLETE", atype: AttributeType::Black }, StringType { name: "AUDIOPROCESS", atype: AttributeType::Black }, StringType { name: "AUDIOSTART", atype: AttributeType::Black }, - StringType { name: "AUTOCOMPLETEERROR", atype: AttributeType::Black }, StringType { name: "AUTOCOMPLETE", atype: AttributeType::Black }, + StringType { name: "AUTOCOMPLETEERROR", atype: AttributeType::Black }, + StringType { name: "AUXCLICK", atype: AttributeType::Black }, + StringType { name: "BACKGROUNDFETCHABORT", atype: AttributeType::Black }, + StringType { name: "BACKGROUNDFETCHCLICK", atype: AttributeType::Black }, + StringType { name: "BACKGROUNDFETCHFAIL", atype: AttributeType::Black }, + StringType { name: "BACKGROUNDFETCHSUCCESS", atype: AttributeType::Black }, StringType { name: "BEFOREACTIVATE", atype: AttributeType::Black }, StringType { name: "BEFORECOPY", atype: AttributeType::Black }, StringType { name: "BEFORECUT", atype: AttributeType::Black }, StringType { name: "BEFOREINPUT", atype: AttributeType::Black }, StringType { name: "BEFORELOAD", atype: AttributeType::Black }, + StringType { name: "BEFOREMATCH", atype: AttributeType::Black }, StringType { name: "BEFOREPASTE", atype: AttributeType::Black }, StringType { name: "BEFOREPRINT", atype: AttributeType::Black }, + StringType { name: "BEFORESCRIPTEXECUTE", atype: AttributeType::Black }, + StringType { name: "BEFORETOGGLE", atype: AttributeType::Black }, StringType { name: "BEFOREUNLOAD", atype: AttributeType::Black }, StringType { name: "BEGINEVENT", atype: AttributeType::Black }, StringType { name: "BLOCKED", atype: AttributeType::Black }, StringType { name: "BLUR", atype: AttributeType::Black }, StringType { name: "BOUNDARY", atype: AttributeType::Black }, StringType { name: "BUFFEREDAMOUNTLOW", atype: AttributeType::Black }, + StringType { name: "BUFFEREDCHANGE", atype: AttributeType::Black }, StringType { name: "CACHED", atype: AttributeType::Black }, StringType { name: "CANCEL", atype: AttributeType::Black }, - StringType { name: "CANPLAYTHROUGH", atype: AttributeType::Black }, StringType { name: "CANPLAY", atype: AttributeType::Black }, + StringType { name: "CANPLAYTHROUGH", atype: AttributeType::Black }, StringType { name: "CHANGE", atype: AttributeType::Black }, StringType { name: "CHARGINGCHANGE", atype: AttributeType::Black }, StringType { name: "CHARGINGTIMECHANGE", atype: AttributeType::Black }, StringType { name: "CHECKING", atype: AttributeType::Black }, StringType { name: "CLICK", atype: AttributeType::Black }, StringType { name: "CLOSE", atype: AttributeType::Black }, + StringType { name: "CLOSING", atype: AttributeType::Black }, StringType { name: "COMPLETE", atype: AttributeType::Black }, StringType { name: "COMPOSITIONEND", atype: AttributeType::Black }, StringType { name: "COMPOSITIONSTART", atype: AttributeType::Black }, + StringType { name: "COMPOSITIONCHANGE", atype: AttributeType::Black }, StringType { name: "COMPOSITIONUPDATE", atype: AttributeType::Black }, + StringType { name: "COMMAND", atype: AttributeType::Black }, + StringType { name: "CONFIGURATIONCHANGE", atype: AttributeType::Black }, + StringType { name: "CONNECT", atype: AttributeType::Black }, StringType { name: "CONNECTING", atype: AttributeType::Black }, StringType { name: "CONNECTIONSTATECHANGE", atype: AttributeType::Black }, - StringType { name: "CONNECT", atype: AttributeType::Black }, + StringType { name: "CONTENTVISIBILITYAUTOSTATECHANGE", atype: AttributeType::Black }, + StringType { name: "CONTEXTLOST", atype: AttributeType::Black }, StringType { name: "CONTEXTMENU", atype: AttributeType::Black }, + StringType { name: "CONTEXTRESTORED", atype: AttributeType::Black }, StringType { name: "CONTROLLERCHANGE", atype: AttributeType::Black }, + StringType { name: "COOKIECHANGE", atype: AttributeType::Black }, + StringType { name: "COORDINATORSTATECHANGE", atype: AttributeType::Black }, StringType { name: "COPY", atype: AttributeType::Black }, + StringType { name: "COUPONCODECHANGED", atype: AttributeType::Black }, StringType { name: "CUECHANGE", atype: AttributeType::Black }, + StringType { name: "CURRENTENTRYCHANGE", atype: AttributeType::Black }, StringType { name: "CUT", atype: AttributeType::Black }, StringType { name: "DATAAVAILABLE", atype: AttributeType::Black }, StringType { name: "DATACHANNEL", atype: AttributeType::Black }, StringType { name: "DBLCLICK", atype: AttributeType::Black }, + StringType { name: "DEQUEUE", atype: AttributeType::Black }, StringType { name: "DEVICECHANGE", atype: AttributeType::Black }, + StringType { name: "DEVICELIGHT", atype: AttributeType::Black }, StringType { name: "DEVICEMOTION", atype: AttributeType::Black }, StringType { name: "DEVICEORIENTATION", atype: AttributeType::Black }, + StringType { name: "DEVICEORIENTATIONABSOLUTE", atype: AttributeType::Black }, StringType { name: "DISCHARGINGTIMECHANGE", atype: AttributeType::Black }, StringType { name: "DISCONNECT", atype: AttributeType::Black }, + StringType { name: "DISPOSE", atype: AttributeType::Black }, StringType { name: "DOMACTIVATE", atype: AttributeType::Black }, StringType { name: "DOMCHARACTERDATAMODIFIED", atype: AttributeType::Black }, StringType { name: "DOMCONTENTLOADED", atype: AttributeType::Black }, - StringType { name: "DOMFOCUSIN", atype: AttributeType::Black }, - StringType { name: "DOMFOCUSOUT", atype: AttributeType::Black }, - StringType { name: "DOMNODEINSERTEDINTODOCUMENT", atype: AttributeType::Black }, StringType { name: "DOMNODEINSERTED", atype: AttributeType::Black }, - StringType { name: "DOMNODEREMOVEDFROMDOCUMENT", atype: AttributeType::Black }, + StringType { name: "DOMNODEINSERTEDINTODOCUMENT", atype: AttributeType::Black }, StringType { name: "DOMNODEREMOVED", atype: AttributeType::Black }, + StringType { name: "DOMNODEREMOVEDFROMDOCUMENT", atype: AttributeType::Black }, StringType { name: "DOMSUBTREEMODIFIED", atype: AttributeType::Black }, StringType { name: "DOWNLOADING", atype: AttributeType::Black }, + StringType { name: "DRAG", atype: AttributeType::Black }, StringType { name: "DRAGEND", atype: AttributeType::Black }, StringType { name: "DRAGENTER", atype: AttributeType::Black }, StringType { name: "DRAGLEAVE", atype: AttributeType::Black }, + StringType { name: "DRAGEXIT", atype: AttributeType::Black }, StringType { name: "DRAGOVER", atype: AttributeType::Black }, StringType { name: "DRAGSTART", atype: AttributeType::Black }, - StringType { name: "DRAG", atype: AttributeType::Black }, StringType { name: "DROP", atype: AttributeType::Black }, StringType { name: "DURATIONCHANGE", atype: AttributeType::Black }, StringType { name: "EMPTIED", atype: AttributeType::Black }, StringType { name: "ENCRYPTED", atype: AttributeType::Black }, + StringType { name: "EDGEUICANCELED", atype: AttributeType::Black }, + StringType { name: "EDGEUICOMPLETED", atype: AttributeType::Black }, + StringType { name: "EDGEUISTARTED", atype: AttributeType::Black }, + StringType { name: "EDITORBEFOREINPUT", atype: AttributeType::Black }, + StringType { name: "EDITORINPUT", atype: AttributeType::Black }, + StringType { name: "END", atype: AttributeType::Black }, StringType { name: "ENDED", atype: AttributeType::Black }, StringType { name: "ENDEVENT", atype: AttributeType::Black }, - StringType { name: "END", atype: AttributeType::Black }, - StringType { name: "ENTERPICTUREINPICTURE", atype: AttributeType::Black }, + StringType { name: "ENDSTREAMING", atype: AttributeType::Black }, StringType { name: "ENTER", atype: AttributeType::Black }, + StringType { name: "ENTERPICTUREINPICTURE", atype: AttributeType::Black }, StringType { name: "ERROR", atype: AttributeType::Black }, StringType { name: "EXIT", atype: AttributeType::Black }, + StringType { name: "FENCEDTREECLICK", atype: AttributeType::Black }, StringType { name: "FETCH", atype: AttributeType::Black }, StringType { name: "FINISH", atype: AttributeType::Black }, + StringType { name: "FOCUS", atype: AttributeType::Black }, StringType { name: "FOCUSIN", atype: AttributeType::Black }, StringType { name: "FOCUSOUT", atype: AttributeType::Black }, - StringType { name: "FOCUS", atype: AttributeType::Black }, StringType { name: "FORMCHANGE", atype: AttributeType::Black }, - StringType { name: "FORMINPUT", atype: AttributeType::Black }, + StringType { name: "FORMCHECKBOXSTATECHANGE", atype: AttributeType::Black }, + StringType { name: "FORMDATA", atype: AttributeType::Black }, + StringType { name: "FORMINVALID", atype: AttributeType::Black }, + StringType { name: "FORMRADIOSTATECHANGE", atype: AttributeType::Black }, + StringType { name: "FORMRESET", atype: AttributeType::Black }, + StringType { name: "FORMSELECT", atype: AttributeType::Black }, + StringType { name: "FORMSUBMIT", atype: AttributeType::Black }, + StringType { name: "FULLSCREENCHANGE", atype: AttributeType::Black }, + StringType { name: "FULLSCREENERROR", atype: AttributeType::Black }, + StringType { name: "GAMEPADAXISMOVE", atype: AttributeType::Black }, + StringType { name: "GAMEPADBUTTONDOWN", atype: AttributeType::Black }, + StringType { name: "GAMEPADBUTTONUP", atype: AttributeType::Black }, StringType { name: "GAMEPADCONNECTED", atype: AttributeType::Black }, StringType { name: "GAMEPADDISCONNECTED", atype: AttributeType::Black }, + StringType { name: "GATHERINGSTATECHANGE", atype: AttributeType::Black }, StringType { name: "GESTURECHANGE", atype: AttributeType::Black }, StringType { name: "GESTUREEND", atype: AttributeType::Black }, StringType { name: "GESTURESCROLLEND", atype: AttributeType::Black }, StringType { name: "GESTURESCROLLSTART", atype: AttributeType::Black }, StringType { name: "GESTURESCROLLUPDATE", atype: AttributeType::Black }, StringType { name: "GESTURESTART", atype: AttributeType::Black }, - StringType { name: "GESTURETAPDOWN", atype: AttributeType::Black }, StringType { name: "GESTURETAP", atype: AttributeType::Black }, + StringType { name: "GESTURETAPDOWN", atype: AttributeType::Black }, StringType { name: "GOTPOINTERCAPTURE", atype: AttributeType::Black }, StringType { name: "HASHCHANGE", atype: AttributeType::Black }, - StringType { name: "ICECANDIDATEERROR", atype: AttributeType::Black }, StringType { name: "ICECANDIDATE", atype: AttributeType::Black }, + StringType { name: "ICECANDIDATEERROR", atype: AttributeType::Black }, StringType { name: "ICECONNECTIONSTATECHANGE", atype: AttributeType::Black }, StringType { name: "ICEGATHERINGSTATECHANGE", atype: AttributeType::Black }, + StringType { name: "IMAGEABORT", atype: AttributeType::Black }, StringType { name: "INACTIVE", atype: AttributeType::Black }, - StringType { name: "INPUTSOURCESCHANGE", atype: AttributeType::Black }, StringType { name: "INPUT", atype: AttributeType::Black }, + StringType { name: "INPUTSOURCESCHANGE", atype: AttributeType::Black }, StringType { name: "INSTALL", atype: AttributeType::Black }, StringType { name: "INVALID", atype: AttributeType::Black }, + StringType { name: "INVOKE", atype: AttributeType::Black }, StringType { name: "KEYDOWN", atype: AttributeType::Black }, StringType { name: "KEYPRESS", atype: AttributeType::Black }, StringType { name: "KEYSTATUSESCHANGE", atype: AttributeType::Black }, StringType { name: "KEYUP", atype: AttributeType::Black }, StringType { name: "LANGUAGECHANGE", atype: AttributeType::Black }, StringType { name: "LEAVEPICTUREINPICTURE", atype: AttributeType::Black }, + StringType { name: "LEGACYATTRMODIFIED", atype: AttributeType::Black }, + StringType { name: "LEGACYCHARACTERDATAMODIFIED", atype: AttributeType::Black }, + StringType { name: "LEGACYDOMACTIVATE", atype: AttributeType::Black }, + StringType { name: "LEGACYDOMFOCUSIN", atype: AttributeType::Black }, + StringType { name: "LEGACYDOMFOCUSOUT", atype: AttributeType::Black }, + StringType { name: "LEGACYMOUSELINEORPAGESCROLL", atype: AttributeType::Black }, + StringType { name: "LEGACYMOUSEPIXELSCROLL", atype: AttributeType::Black }, + StringType { name: "LEGACYNODEINSERTED", atype: AttributeType::Black }, + StringType { name: "LEGACYNODEINSERTEDINTODOCUMENT", atype: AttributeType::Black }, + StringType { name: "LEGACYNODEREMOVED", atype: AttributeType::Black }, + StringType { name: "LEGACYNODEREMOVEDFROMDOCUMENT", atype: AttributeType::Black }, + StringType { name: "LEGACYSUBTREEMODIFIED", atype: AttributeType::Black }, + StringType { name: "LEGACYTEXTINPUT", atype: AttributeType::Black }, StringType { name: "LEVELCHANGE", atype: AttributeType::Black }, + StringType { name: "LOAD", atype: AttributeType::Black }, StringType { name: "LOADEDDATA", atype: AttributeType::Black }, StringType { name: "LOADEDMETADATA", atype: AttributeType::Black }, StringType { name: "LOADEND", atype: AttributeType::Black }, + StringType { name: "LOADING", atype: AttributeType::Black }, StringType { name: "LOADINGDONE", atype: AttributeType::Black }, StringType { name: "LOADINGERROR", atype: AttributeType::Black }, - StringType { name: "LOADING", atype: AttributeType::Black }, StringType { name: "LOADSTART", atype: AttributeType::Black }, - StringType { name: "LOAD", atype: AttributeType::Black }, StringType { name: "LOSTPOINTERCAPTURE", atype: AttributeType::Black }, + StringType { name: "MAGNIFYGESTURE", atype: AttributeType::Black }, + StringType { name: "MAGNIFYGESTURESTART", atype: AttributeType::Black }, + StringType { name: "MAGNIFYGESTUREUPDATE", atype: AttributeType::Black }, StringType { name: "MARK", atype: AttributeType::Black }, + StringType { name: "MEDIARECORDERDATAAVAILABLE", atype: AttributeType::Black }, + StringType { name: "MEDIARECORDERSTOP", atype: AttributeType::Black }, + StringType { name: "MEDIARECORDERWARNING", atype: AttributeType::Black }, StringType { name: "MERCHANTVALIDATION", atype: AttributeType::Black }, - StringType { name: "MESSAGEERROR", atype: AttributeType::Black }, StringType { name: "MESSAGE", atype: AttributeType::Black }, + StringType { name: "MESSAGEERROR", atype: AttributeType::Black }, + StringType { name: "MOUSEDOUBLECLICK", atype: AttributeType::Black }, StringType { name: "MOUSEDOWN", atype: AttributeType::Black }, StringType { name: "MOUSEENTER", atype: AttributeType::Black }, + StringType { name: "MOUSEEXPLOREBYTOUCH", atype: AttributeType::Black }, + StringType { name: "MOUSEHITTEST", atype: AttributeType::Black }, StringType { name: "MOUSELEAVE", atype: AttributeType::Black }, + StringType { name: "MOUSELONGTAP", atype: AttributeType::Black }, StringType { name: "MOUSEMOVE", atype: AttributeType::Black }, StringType { name: "MOUSEOUT", atype: AttributeType::Black }, StringType { name: "MOUSEOVER", atype: AttributeType::Black }, StringType { name: "MOUSEUP", atype: AttributeType::Black }, StringType { name: "MOUSEWHEEL", atype: AttributeType::Black }, + StringType { name: "MOZFULLSCREENCHANGE", atype: AttributeType::Black }, + StringType { name: "MOZFULLSCREENERROR", atype: AttributeType::Black }, + StringType { name: "MOZPOINTERLOCKCHANGE", atype: AttributeType::Black }, + StringType { name: "MOZPOINTERLOCKERROR", atype: AttributeType::Black }, + StringType { name: "MOZVISUALRESIZE", atype: AttributeType::Black }, + StringType { name: "MOZVISUALSCROLL", atype: AttributeType::Black }, StringType { name: "MUTE", atype: AttributeType::Black }, + StringType { name: "NAVIGATE", atype: AttributeType::Black }, + StringType { name: "NAVIGATEERROR", atype: AttributeType::Black }, + StringType { name: "NAVIGATESUCCESS", atype: AttributeType::Black }, StringType { name: "NEGOTIATIONNEEDED", atype: AttributeType::Black }, StringType { name: "NEXTTRACK", atype: AttributeType::Black }, StringType { name: "NOMATCH", atype: AttributeType::Black }, + StringType { name: "NOTIFICATIONCLICK", atype: AttributeType::Black }, + StringType { name: "NOTIFICATIONCLOSE", atype: AttributeType::Black }, StringType { name: "NOUPDATE", atype: AttributeType::Black }, StringType { name: "OBSOLETE", atype: AttributeType::Black }, StringType { name: "OFFLINE", atype: AttributeType::Black }, StringType { name: "ONLINE", atype: AttributeType::Black }, StringType { name: "OPEN", atype: AttributeType::Black }, StringType { name: "ORIENTATIONCHANGE", atype: AttributeType::Black }, - StringType { name: "OVERCONSTRAINED", atype: AttributeType::Black }, StringType { name: "OVERFLOWCHANGED", atype: AttributeType::Black }, + StringType { name: "OVERSCROLL", atype: AttributeType::Black }, StringType { name: "PAGEHIDE", atype: AttributeType::Black }, + StringType { name: "PAGEREVEAL", atype: AttributeType::Black }, StringType { name: "PAGESHOW", atype: AttributeType::Black }, + StringType { name: "PAGESWAP", atype: AttributeType::Black }, StringType { name: "PASTE", atype: AttributeType::Black }, StringType { name: "PAUSE", atype: AttributeType::Black }, StringType { name: "PAYERDETAILCHANGE", atype: AttributeType::Black }, StringType { name: "PAYMENTAUTHORIZED", atype: AttributeType::Black }, StringType { name: "PAYMENTMETHODCHANGE", atype: AttributeType::Black }, StringType { name: "PAYMENTMETHODSELECTED", atype: AttributeType::Black }, - StringType { name: "PLAYING", atype: AttributeType::Black }, StringType { name: "PLAY", atype: AttributeType::Black }, + StringType { name: "PLAYING", atype: AttributeType::Black }, + StringType { name: "POINTERAUXCLICK", atype: AttributeType::Black }, StringType { name: "POINTERCANCEL", atype: AttributeType::Black }, + StringType { name: "POINTERCLICK", atype: AttributeType::Black }, StringType { name: "POINTERDOWN", atype: AttributeType::Black }, StringType { name: "POINTERENTER", atype: AttributeType::Black }, + StringType { name: "POINTERGOTCAPTURE", atype: AttributeType::Black }, StringType { name: "POINTERLEAVE", atype: AttributeType::Black }, StringType { name: "POINTERLOCKCHANGE", atype: AttributeType::Black }, StringType { name: "POINTERLOCKERROR", atype: AttributeType::Black }, + StringType { name: "POINTERLOSTCAPTURE", atype: AttributeType::Black }, StringType { name: "POINTERMOVE", atype: AttributeType::Black }, StringType { name: "POINTEROUT", atype: AttributeType::Black }, StringType { name: "POINTEROVER", atype: AttributeType::Black }, + StringType { name: "POINTERRAWUPDATE", atype: AttributeType::Black }, StringType { name: "POINTERUP", atype: AttributeType::Black }, StringType { name: "POPSTATE", atype: AttributeType::Black }, + StringType { name: "PRESSTAPGESTURE", atype: AttributeType::Black }, StringType { name: "PREVIOUSTRACK", atype: AttributeType::Black }, + StringType { name: "PROPERTYCHANGE", atype: AttributeType::Black }, StringType { name: "PROCESSORERROR", atype: AttributeType::Black }, StringType { name: "PROGRESS", atype: AttributeType::Black }, - StringType { name: "PROPERTYCHANGE", atype: AttributeType::Black }, + StringType { name: "PUSH", atype: AttributeType::Black }, + StringType { name: "PUSHNOTIFICATION", atype: AttributeType::Black }, + StringType { name: "PUSHSUBSCRIPTIONCHANGE", atype: AttributeType::Black }, + StringType { name: "QUALITYCHANGE", atype: AttributeType::Black }, StringType { name: "RATECHANGE", atype: AttributeType::Black }, StringType { name: "READYSTATECHANGE", atype: AttributeType::Black }, + StringType { name: "REDRAW", atype: AttributeType::Black }, StringType { name: "REJECTIONHANDLED", atype: AttributeType::Black }, + StringType { name: "RELEASE", atype: AttributeType::Black }, + StringType { name: "REMOVE", atype: AttributeType::Black }, StringType { name: "REMOVESOURCEBUFFER", atype: AttributeType::Black }, StringType { name: "REMOVESTREAM", atype: AttributeType::Black }, StringType { name: "REMOVETRACK", atype: AttributeType::Black }, - StringType { name: "REMOVE", atype: AttributeType::Black }, + StringType { name: "REPEAT", atype: AttributeType::Black }, + StringType { name: "REPEATEVENT", atype: AttributeType::Black }, StringType { name: "RESET", atype: AttributeType::Black }, StringType { name: "RESIZE", atype: AttributeType::Black }, StringType { name: "RESOURCETIMINGBUFFERFULL", atype: AttributeType::Black }, StringType { name: "RESULT", atype: AttributeType::Black }, StringType { name: "RESUME", atype: AttributeType::Black }, + StringType { name: "ROTATEGESTURE", atype: AttributeType::Black }, + StringType { name: "ROTATEGESTURESTART", atype: AttributeType::Black }, + StringType { name: "ROTATEGESTUREUPDATE", atype: AttributeType::Black }, + StringType { name: "RTCTRANSFORM", atype: AttributeType::Black }, StringType { name: "SCROLL", atype: AttributeType::Black }, + StringType { name: "SCROLLEDAREACHANGED", atype: AttributeType::Black }, + StringType { name: "SCROLLEND", atype: AttributeType::Black }, + StringType { name: "SCROLLPORTOVERFLOW", atype: AttributeType::Black }, + StringType { name: "SCROLLPORTUNDERFLOW", atype: AttributeType::Black }, + StringType { name: "SCROLLSNAPCHANGE", atype: AttributeType::Black }, + StringType { name: "SCROLLSNAPCHANGING", atype: AttributeType::Black }, StringType { name: "SEARCH", atype: AttributeType::Black }, StringType { name: "SECURITYPOLICYVIOLATION", atype: AttributeType::Black }, StringType { name: "SEEKED", atype: AttributeType::Black }, StringType { name: "SEEKING", atype: AttributeType::Black }, + StringType { name: "SELECT", atype: AttributeType::Black }, + StringType { name: "SELECTEDCANDIDATEPAIRCHANGE", atype: AttributeType::Black }, StringType { name: "SELECTEND", atype: AttributeType::Black }, StringType { name: "SELECTIONCHANGE", atype: AttributeType::Black }, StringType { name: "SELECTSTART", atype: AttributeType::Black }, - StringType { name: "SELECT", atype: AttributeType::Black }, StringType { name: "SHIPPINGADDRESSCHANGE", atype: AttributeType::Black }, StringType { name: "SHIPPINGCONTACTSELECTED", atype: AttributeType::Black }, StringType { name: "SHIPPINGMETHODSELECTED", atype: AttributeType::Black }, @@ -242,6 +351,10 @@ pub const BLACK_ATTR_EVENTS: &[StringType] = &[ StringType { name: "SHOW", atype: AttributeType::Black }, StringType { name: "SIGNALINGSTATECHANGE", atype: AttributeType::Black }, StringType { name: "SLOTCHANGE", atype: AttributeType::Black }, + StringType { name: "SMILBEGINEVENT", atype: AttributeType::Black }, + StringType { name: "SMILENDEVENT", atype: AttributeType::Black }, + StringType { name: "SMILREPEATEVENT", atype: AttributeType::Black }, + StringType { name: "SORT", atype: AttributeType::Black }, StringType { name: "SOUNDEND", atype: AttributeType::Black }, StringType { name: "SOUNDSTART", atype: AttributeType::Black }, StringType { name: "SOURCECLOSE", atype: AttributeType::Black }, @@ -249,18 +362,27 @@ pub const BLACK_ATTR_EVENTS: &[StringType] = &[ StringType { name: "SOURCEOPEN", atype: AttributeType::Black }, StringType { name: "SPEECHEND", atype: AttributeType::Black }, StringType { name: "SPEECHSTART", atype: AttributeType::Black }, + StringType { name: "SQUEEZE", atype: AttributeType::Black }, StringType { name: "SQUEEZEEND", atype: AttributeType::Black }, StringType { name: "SQUEEZESTART", atype: AttributeType::Black }, - StringType { name: "SQUEEZE", atype: AttributeType::Black }, StringType { name: "STALLED", atype: AttributeType::Black }, - StringType { name: "STARTED", atype: AttributeType::Black }, StringType { name: "START", atype: AttributeType::Black }, + StringType { name: "STARTED", atype: AttributeType::Black }, + StringType { name: "STARTSTREAMING", atype: AttributeType::Black }, StringType { name: "STATECHANGE", atype: AttributeType::Black }, StringType { name: "STOP", atype: AttributeType::Black }, StringType { name: "STORAGE", atype: AttributeType::Black }, StringType { name: "SUBMIT", atype: AttributeType::Black }, + StringType { name: "SVGLOAD", atype: AttributeType::Black }, + StringType { name: "SVGSCROLL", atype: AttributeType::Black }, + StringType { name: "SWIPEGESTURE", atype: AttributeType::Black }, + StringType { name: "SWIPEGESTUREEND", atype: AttributeType::Black }, + StringType { name: "SWIPEGESTUREMAYSTART", atype: AttributeType::Black }, + StringType { name: "SWIPEGESTURESTART", atype: AttributeType::Black }, + StringType { name: "SWIPEGESTUREUPDATE", atype: AttributeType::Black }, StringType { name: "SUCCESS", atype: AttributeType::Black }, StringType { name: "SUSPEND", atype: AttributeType::Black }, + StringType { name: "TAPGESTURE", atype: AttributeType::Black }, StringType { name: "TEXTINPUT", atype: AttributeType::Black }, StringType { name: "TIMEOUT", atype: AttributeType::Black }, StringType { name: "TIMEUPDATE", atype: AttributeType::Black }, @@ -278,27 +400,36 @@ pub const BLACK_ATTR_EVENTS: &[StringType] = &[ StringType { name: "TRANSITIONSTART", atype: AttributeType::Black }, StringType { name: "UNCAPTUREDERROR", atype: AttributeType::Black }, StringType { name: "UNHANDLEDREJECTION", atype: AttributeType::Black }, + StringType { name: "UNIDENTIFIEDEVENT", atype: AttributeType::Black }, StringType { name: "UNLOAD", atype: AttributeType::Black }, StringType { name: "UNMUTE", atype: AttributeType::Black }, + StringType { name: "USERPROXIMITY", atype: AttributeType::Black }, + StringType { name: "UPDATE", atype: AttributeType::Black }, StringType { name: "UPDATEEND", atype: AttributeType::Black }, StringType { name: "UPDATEFOUND", atype: AttributeType::Black }, StringType { name: "UPDATEREADY", atype: AttributeType::Black }, StringType { name: "UPDATESTART", atype: AttributeType::Black }, - StringType { name: "UPDATE", atype: AttributeType::Black }, StringType { name: "UPGRADENEEDED", atype: AttributeType::Black }, StringType { name: "VALIDATEMERCHANT", atype: AttributeType::Black }, StringType { name: "VERSIONCHANGE", atype: AttributeType::Black }, StringType { name: "VISIBILITYCHANGE", atype: AttributeType::Black }, + StringType { name: "VOICESCHANGED", atype: AttributeType::Black }, StringType { name: "VOLUMECHANGE", atype: AttributeType::Black }, - StringType { name: "WAITINGFORKEY", atype: AttributeType::Black }, + StringType { name: "VRDISPLAYACTIVATE", atype: AttributeType::Black }, + StringType { name: "VRDISPLAYCONNECT", atype: AttributeType::Black }, + StringType { name: "VRDISPLAYDEACTIVATE", atype: AttributeType::Black }, + StringType { name: "VRDISPLAYDISCONNECT", atype: AttributeType::Black }, + StringType { name: "VRDISPLAYPRESENTCHANGE", atype: AttributeType::Black }, StringType { name: "WAITING", atype: AttributeType::Black }, - StringType { name: "WEBGLCONTEXTCHANGED", atype: AttributeType::Black }, + StringType { name: "WAITINGFORKEY", atype: AttributeType::Black }, StringType { name: "WEBGLCONTEXTCREATIONERROR", atype: AttributeType::Black }, StringType { name: "WEBGLCONTEXTLOST", atype: AttributeType::Black }, StringType { name: "WEBGLCONTEXTRESTORED", atype: AttributeType::Black }, StringType { name: "WEBKITANIMATIONEND", atype: AttributeType::Black }, StringType { name: "WEBKITANIMATIONITERATION", atype: AttributeType::Black }, StringType { name: "WEBKITANIMATIONSTART", atype: AttributeType::Black }, + StringType { name: "WEBKITASSOCIATEFORMCONTROLS", atype: AttributeType::Black }, + StringType { name: "WEBKITAUTOFILLREQUEST", atype: AttributeType::Black }, StringType { name: "WEBKITBEFORETEXTINSERTED", atype: AttributeType::Black }, StringType { name: "WEBKITBEGINFULLSCREEN", atype: AttributeType::Black }, StringType { name: "WEBKITCURRENTPLAYBACKTARGETISWIRELESSCHANGED", atype: AttributeType::Black }, @@ -308,6 +439,7 @@ pub const BLACK_ATTR_EVENTS: &[StringType] = &[ StringType { name: "WEBKITKEYADDED", atype: AttributeType::Black }, StringType { name: "WEBKITKEYERROR", atype: AttributeType::Black }, StringType { name: "WEBKITKEYMESSAGE", atype: AttributeType::Black }, + StringType { name: "WEBKITMEDIASESSIONMETADATACHANGED", atype: AttributeType::Black }, StringType { name: "WEBKITMOUSEFORCECHANGED", atype: AttributeType::Black }, StringType { name: "WEBKITMOUSEFORCEDOWN", atype: AttributeType::Black }, StringType { name: "WEBKITMOUSEFORCEUP", atype: AttributeType::Black }, @@ -316,21 +448,23 @@ pub const BLACK_ATTR_EVENTS: &[StringType] = &[ StringType { name: "WEBKITNETWORKINFOCHANGE", atype: AttributeType::Black }, StringType { name: "WEBKITPLAYBACKTARGETAVAILABILITYCHANGED", atype: AttributeType::Black }, StringType { name: "WEBKITPRESENTATIONMODECHANGED", atype: AttributeType::Black }, - StringType { name: "WEBKITREGIONOVERSETCHANGE", atype: AttributeType::Black }, StringType { name: "WEBKITREMOVESOURCEBUFFER", atype: AttributeType::Black }, + StringType { name: "WEBKITSHADOWROOTATTACHED", atype: AttributeType::Black }, StringType { name: "WEBKITSOURCECLOSE", atype: AttributeType::Black }, StringType { name: "WEBKITSOURCEENDED", atype: AttributeType::Black }, StringType { name: "WEBKITSOURCEOPEN", atype: AttributeType::Black }, - StringType { name: "WEBKITSPEECHCHANGE", atype: AttributeType::Black }, StringType { name: "WEBKITTRANSITIONEND", atype: AttributeType::Black }, - StringType { name: "WEBKITWILLREVEALBOTTOM", atype: AttributeType::Black }, - StringType { name: "WEBKITWILLREVEALLEFT", atype: AttributeType::Black }, - StringType { name: "WEBKITWILLREVEALRIGHT", atype: AttributeType::Black }, - StringType { name: "WEBKITWILLREVEALTOP", atype: AttributeType::Black }, StringType { name: "WHEEL", atype: AttributeType::Black }, + StringType { name: "WRITE", atype: AttributeType::Black }, StringType { name: "WRITEEND", atype: AttributeType::Black }, StringType { name: "WRITESTART", atype: AttributeType::Black }, - StringType { name: "WRITE", atype: AttributeType::Black }, + StringType { name: "XULBROADCAST", atype: AttributeType::Black }, + StringType { name: "XULCOMMANDUPDATE", atype: AttributeType::Black }, + StringType { name: "XULPOPUPHIDDEN", atype: AttributeType::Black }, + StringType { name: "XULPOPUPHIDING", atype: AttributeType::Black }, + StringType { name: "XULPOPUPSHOWING", atype: AttributeType::Black }, + StringType { name: "XULPOPUPSHOWN", atype: AttributeType::Black }, + StringType { name: "XULSYSTEMSTATUSBARCLICK", atype: AttributeType::Black }, StringType { name: "ZOOM", atype: AttributeType::Black }, ]; @@ -391,6 +525,11 @@ pub const BLACK_URL_PROTOCOLS: &[&str] = &[ "JAVASCRIPT", ]; +// IS_HEX_ENTITY_PREFIX: callers must ensure src.len() >= 3 +fn is_hex_entity_prefix(src: &[u8]) -> bool { + src[2] == b'x' || src[2] == b'X' +} + pub fn html_decode_char_at(src: &[u8], consumed: &mut usize) -> i32 { if src.is_empty() { *consumed = 0; @@ -398,7 +537,9 @@ pub fn html_decode_char_at(src: &[u8], consumed: &mut usize) -> i32 { } *consumed = 1; - if src[0] != b'&' || src.len() < 2 { + // Decode only "&" with len >= 3, or len >= 4 after a hex prefix; + // otherwise return the character itself. + if src[0] != b'&' || src.len() < 3 || (is_hex_entity_prefix(src) && src.len() < 4) { return src[0] as i32; } @@ -407,12 +548,8 @@ pub fn html_decode_char_at(src: &[u8], consumed: &mut usize) -> i32 { return b'&' as i32; } - if src.len() > 2 && (src[2] == b'x' || src[2] == b'X') { + if is_hex_entity_prefix(src) { // Hexadecimal entity - if src.len() < 4 { - return b'&' as i32; - } - let ch = HEX_DECODE_MAP[src[3] as usize]; if ch == 256 { return b'&' as i32; @@ -442,10 +579,6 @@ pub fn html_decode_char_at(src: &[u8], consumed: &mut usize) -> i32 { } else { // Decimal entity let mut i = 2; - if i >= src.len() { - return b'&' as i32; - } - let ch = src[i]; if ch < b'0' || ch > b'9' { return b'&' as i32; @@ -472,4 +605,4 @@ pub fn html_decode_char_at(src: &[u8], consumed: &mut usize) -> i32 { *consumed = i; val } -} \ No newline at end of file +} diff --git a/libinjectionrs/src/xss/detector.rs b/libinjectionrs/src/xss/detector.rs index d926ad6..53777c1 100644 --- a/libinjectionrs/src/xss/detector.rs +++ b/libinjectionrs/src/xss/detector.rs @@ -175,14 +175,15 @@ impl XssDetector { if attr_name.len() >= 5 { if (attr_name[0] == b'o' || attr_name[0] == b'O') && (attr_name[1] == b'n' || attr_name[1] == b'N') { - let event_name = &attr_name[2..]; + let s_without_on = &attr_name[2..]; for event in BLACK_ATTR_EVENTS { - // C compares only strlen(black->name) chars, so anything + // C compares at most strlen(black->name) chars, so anything // after the event name is ignored: `onerror%09` matches on // the `error` prefix. The general BLACKATTR check below // uses the full length, so only this one is limited. let name = event.name.as_bytes(); - if Self::cstrcasecmp_with_null_limited(name, event_name, name.len()) { + let max_len = s_without_on.len().min(name.len()); + if Self::cstrcasecmp_with_null_limited(name, s_without_on, max_len) { return event.atype; } } diff --git a/libinjectionrs/src/xss/html5.rs b/libinjectionrs/src/xss/html5.rs index c72d81a..ccb6301 100644 --- a/libinjectionrs/src/xss/html5.rs +++ b/libinjectionrs/src/xss/html5.rs @@ -142,14 +142,14 @@ impl<'a> Html5State<'a> { None } - // Match C h5_skip_white exactly: includes 0x00 for IE compatibility - // CRITICAL: C uses signed char, so any byte >= 128 becomes negative and is returned as-is - fn h5_skip_white(&mut self) -> Option { + // Match C h5_skip_white exactly: includes 0x00 for IE compatibility. + // Returns the next non-whitespace byte as unsigned, or None for CHAR_EOF, + // so a 0xFF byte is an ordinary byte, not end of input. + fn h5_skip_white(&mut self) -> Option { while self.pos < self.len { - let ch_unsigned = self.s[self.pos]; - let ch_signed = ch_unsigned as i8; // Convert to signed like C does: char ch = hs->s[hs->pos] - - match ch_signed { + let ch = self.s[self.pos]; // ch = (unsigned char)hs->s[hs->pos] + + match ch { 0x00 | 0x20 | 0x09 | 0x0A => { // case 0x00, case 0x20, etc. self.pos += 1; } @@ -157,14 +157,11 @@ impl<'a> Html5State<'a> { self.pos += 1; } _ => { - // default: return ch; - // In C, this returns the signed char value - // For 0xFF, this returns -1 (CHAR_EOF) - return Some(ch_signed); + return Some(ch); // default: return ch; } } } - Some(-1) // CHAR_EOF + None // CHAR_EOF } fn find_byte(&self, byte: u8, start: usize) -> Option { @@ -453,8 +450,8 @@ impl<'a> Html5State<'a> { // Manual tail call optimization loop loop { match self.h5_skip_white() { - Some(-1) => return false, // CHAR_EOF - Some(0x2f) => { // CHAR_SLASH (47 as i8) + None => return false, // CHAR_EOF + Some(0x2f) => { // CHAR_SLASH self.advance(); // Tail call optimization: if next char is not '>', loop instead of recursing if self.pos < self.len && self.s[self.pos] != b'>' { @@ -462,7 +459,7 @@ impl<'a> Html5State<'a> { } return self.state_self_closing_start_tag(); } - Some(0x3e) => { // CHAR_GT (62 as i8) + Some(0x3e) => { // CHAR_GT self.set_token(TokenType::TagNameClose, self.pos, 1); self.advance(); self.state_fn = Self::state_data; @@ -472,7 +469,6 @@ impl<'a> Html5State<'a> { self.state_fn = Self::state_attribute_name; return self.next(); } - None => return false, // Should not happen with new implementation } } } @@ -520,29 +516,28 @@ impl<'a> Html5State<'a> { fn state_after_attribute_name(&mut self) -> bool { // Match C implementation exactly line by line match self.h5_skip_white() { // c = h5_skip_white(hs) - Some(-1) => false, // case CHAR_EOF: return 0 - Some(0x2f) => { // case CHAR_SLASH (47 as i8) + None => false, // case CHAR_EOF: return 0 + Some(0x2f) => { // case CHAR_SLASH self.pos += 1; // hs->pos += 1 self.state_self_closing_start_tag() // return h5_state_self_closing_start_tag(hs) } - Some(0x3d) => { // case CHAR_EQUALS (61 as i8) + Some(0x3d) => { // case CHAR_EQUALS self.pos += 1; // hs->pos += 1 self.state_before_attribute_value() // return h5_state_before_attribute_value(hs) } - Some(0x3e) => { // case CHAR_GT (62 as i8) + Some(0x3e) => { // case CHAR_GT self.state_tag_name_close() // return h5_state_tag_name_close(hs) } Some(_) => { // default self.state_attribute_name() // return h5_state_attribute_name(hs) } - None => false, // Should not happen with new implementation } } fn state_before_attribute_value(&mut self) -> bool { // Match C implementation exactly: c = h5_skip_white(hs) match self.h5_skip_white() { - Some(-1) => { // case CHAR_EOF + None => { // case CHAR_EOF self.state_fn = Self::state_eof; false } @@ -550,10 +545,6 @@ impl<'a> Html5State<'a> { Some(0x27) => self.state_attribute_value_single_quote(), // CHAR_SINGLE (39) Some(0x60) => self.state_attribute_value_back_quote(), // CHAR_TICK (96) Some(_) => self.state_attribute_value_no_quote(), // default - None => { // Should not happen with new implementation - self.state_fn = Self::state_eof; - false - } } } diff --git a/libinjectionrs/src/xss/tests.rs b/libinjectionrs/src/xss/tests.rs index 5772fd9..4c472fe 100644 --- a/libinjectionrs/src/xss/tests.rs +++ b/libinjectionrs/src/xss/tests.rs @@ -47,8 +47,9 @@ fn test_style_attribute() { #[test] fn test_fuzz_differential_8ce9746b() { - // Fuzz test case where Rust returns true (XSS) but C returns false (safe) - // Input: "�\u{1}\u{1}

�}\r�\u{1}�\u{1}\u{1}

�}\r�\u{1}"; + let mut h5 = Html5State::new(input, Html5Flags::DataState); + let mut saw_attr = false; + while h5.next() { + if h5.token_type == TokenType::AttrName && &h5.token_start[..h5.token_len] == b"onfocus" { + saw_attr = true; + } + } + assert!(saw_attr, "the onfocus attribute after 0xFF must be tokenized"); + assert_eq!(XssDetector::new().detect(input), XssResult::Xss); +}