diff --git a/xss_decls.go b/xss_decls.go index 6a02ec1..4f227f4 100644 --- a/xss_decls.go +++ b/xss_decls.go @@ -13,6 +13,332 @@ type stringType struct { attributeType int } +// You can get these using: +// +// curl https://raw.githubusercontent.com/WebKit/WebKit/main/Source/WebCore/dom/EventNames.json | \ +// jq -r 'keys[] as $k | $k | ascii_upcase | "{\"" + . +"\", attributeTypeBlack },"' | sort +var blackEvents = []stringType{ + {"ABORT", attributeTypeBlack}, + {"ACTIVATE", attributeTypeBlack}, + {"ACTIVE", attributeTypeBlack}, + {"ADDSOURCEBUFFER", attributeTypeBlack}, + {"ADDSTREAM", attributeTypeBlack}, + {"ADDTRACK", attributeTypeBlack}, + {"AFTERPRINT", attributeTypeBlack}, + {"ANIMATIONCANCEL", attributeTypeBlack}, + {"ANIMATIONEND", attributeTypeBlack}, + {"ANIMATIONITERATION", attributeTypeBlack}, + {"ANIMATIONSTART", attributeTypeBlack}, + {"AUDIOEND", attributeTypeBlack}, + {"AUDIOPROCESS", attributeTypeBlack}, + {"AUDIOSTART", attributeTypeBlack}, + {"AUTOCOMPLETE", attributeTypeBlack}, + {"AUTOCOMPLETEERROR", attributeTypeBlack}, + {"BACKGROUNDFETCHABORT", attributeTypeBlack}, + {"BACKGROUNDFETCHCLICK", attributeTypeBlack}, + {"BACKGROUNDFETCHFAIL", attributeTypeBlack}, + {"BACKGROUNDFETCHSUCCESS", attributeTypeBlack}, + {"BEFORECOPY", attributeTypeBlack}, + {"BEFORECUT", attributeTypeBlack}, + {"BEFOREINPUT", attributeTypeBlack}, + {"BEFORELOAD", attributeTypeBlack}, + {"BEFOREPASTE", attributeTypeBlack}, + {"BEFOREPRINT", attributeTypeBlack}, + {"BEFORETOGGLE", attributeTypeBlack}, + {"BEFOREUNLOAD", attributeTypeBlack}, + {"BEGINEVENT", attributeTypeBlack}, + {"BLOCKED", attributeTypeBlack}, + {"BLUR", attributeTypeBlack}, + {"BOUNDARY", attributeTypeBlack}, + {"BUFFEREDAMOUNTLOW", attributeTypeBlack}, + {"BUFFEREDCHANGE", attributeTypeBlack}, + {"CACHED", attributeTypeBlack}, + {"CANCEL", attributeTypeBlack}, + {"CANPLAY", attributeTypeBlack}, + {"CANPLAYTHROUGH", attributeTypeBlack}, + {"CHANGE", attributeTypeBlack}, + {"CHARGINGCHANGE", attributeTypeBlack}, + {"CHARGINGTIMECHANGE", attributeTypeBlack}, + {"CHECKING", attributeTypeBlack}, + {"CLICK", attributeTypeBlack}, + {"CLOSE", attributeTypeBlack}, + {"CLOSING", attributeTypeBlack}, + {"COMPLETE", attributeTypeBlack}, + {"COMPOSITIONEND", attributeTypeBlack}, + {"COMPOSITIONSTART", attributeTypeBlack}, + {"COMPOSITIONUPDATE", attributeTypeBlack}, + {"CONFIGURATIONCHANGE", attributeTypeBlack}, + {"CONNECT", attributeTypeBlack}, + {"CONNECTING", attributeTypeBlack}, + {"CONNECTIONSTATECHANGE", attributeTypeBlack}, + {"CONTENTVISIBILITYAUTOSTATECHANGE", attributeTypeBlack}, + {"CONTEXTMENU", attributeTypeBlack}, + {"CONTROLLERCHANGE", attributeTypeBlack}, + {"COOKIECHANGE", attributeTypeBlack}, + {"COORDINATORSTATECHANGE", attributeTypeBlack}, + {"COPY", attributeTypeBlack}, + {"COUPONCODECHANGED", attributeTypeBlack}, + {"CUECHANGE", attributeTypeBlack}, + {"CURRENTENTRYCHANGE", attributeTypeBlack}, + {"CUT", attributeTypeBlack}, + {"DATAAVAILABLE", attributeTypeBlack}, + {"DATACHANNEL", attributeTypeBlack}, + {"DBLCLICK", attributeTypeBlack}, + {"DEQUEUE", attributeTypeBlack}, + {"DEVICECHANGE", attributeTypeBlack}, + {"DEVICEMOTION", attributeTypeBlack}, + {"DEVICEORIENTATION", attributeTypeBlack}, + {"DISCHARGINGTIMECHANGE", attributeTypeBlack}, + {"DISCONNECT", attributeTypeBlack}, + {"DISPOSE", attributeTypeBlack}, + {"DOMACTIVATE", attributeTypeBlack}, + {"DOMCHARACTERDATAMODIFIED", attributeTypeBlack}, + {"DOMCONTENTLOADED", attributeTypeBlack}, + {"DOMNODEINSERTED", attributeTypeBlack}, + {"DOMNODEINSERTEDINTODOCUMENT", attributeTypeBlack}, + {"DOMNODEREMOVED", attributeTypeBlack}, + {"DOMNODEREMOVEDFROMDOCUMENT", attributeTypeBlack}, + {"DOMSUBTREEMODIFIED", attributeTypeBlack}, + {"DOWNLOADING", attributeTypeBlack}, + {"DRAG", attributeTypeBlack}, + {"DRAGEND", attributeTypeBlack}, + {"DRAGENTER", attributeTypeBlack}, + {"DRAGLEAVE", attributeTypeBlack}, + {"DRAGOVER", attributeTypeBlack}, + {"DRAGSTART", attributeTypeBlack}, + {"DROP", attributeTypeBlack}, + {"DURATIONCHANGE", attributeTypeBlack}, + {"EMPTIED", attributeTypeBlack}, + {"ENCRYPTED", attributeTypeBlack}, + {"END", attributeTypeBlack}, + {"ENDED", attributeTypeBlack}, + {"ENDEVENT", attributeTypeBlack}, + {"ENDSTREAMING", attributeTypeBlack}, + {"ENTER", attributeTypeBlack}, + {"ENTERPICTUREINPICTURE", attributeTypeBlack}, + {"ERROR", attributeTypeBlack}, + {"EXIT", attributeTypeBlack}, + {"FETCH", attributeTypeBlack}, + {"FINISH", attributeTypeBlack}, + {"FOCUS", attributeTypeBlack}, + {"FOCUSIN", attributeTypeBlack}, + {"FOCUSOUT", attributeTypeBlack}, + {"FORMDATA", attributeTypeBlack}, + {"FULLSCREENCHANGE", attributeTypeBlack}, + {"FULLSCREENERROR", attributeTypeBlack}, + {"GAMEPADCONNECTED", attributeTypeBlack}, + {"GAMEPADDISCONNECTED", attributeTypeBlack}, + {"GATHERINGSTATECHANGE", attributeTypeBlack}, + {"GESTURECHANGE", attributeTypeBlack}, + {"GESTUREEND", attributeTypeBlack}, + {"GESTURESCROLLEND", attributeTypeBlack}, + {"GESTURESCROLLSTART", attributeTypeBlack}, + {"GESTURESCROLLUPDATE", attributeTypeBlack}, + {"GESTURESTART", attributeTypeBlack}, + {"GESTURETAP", attributeTypeBlack}, + {"GESTURETAPDOWN", attributeTypeBlack}, + {"GOTPOINTERCAPTURE", attributeTypeBlack}, + {"HASHCHANGE", attributeTypeBlack}, + {"ICECANDIDATE", attributeTypeBlack}, + {"ICECANDIDATEERROR", attributeTypeBlack}, + {"ICECONNECTIONSTATECHANGE", attributeTypeBlack}, + {"ICEGATHERINGSTATECHANGE", attributeTypeBlack}, + {"INACTIVE", attributeTypeBlack}, + {"INPUT", attributeTypeBlack}, + {"INPUTSOURCESCHANGE", attributeTypeBlack}, + {"INSTALL", attributeTypeBlack}, + {"INVALID", attributeTypeBlack}, + {"INVOKE", attributeTypeBlack}, + {"KEYDOWN", attributeTypeBlack}, + {"KEYPRESS", attributeTypeBlack}, + {"KEYSTATUSESCHANGE", attributeTypeBlack}, + {"KEYUP", attributeTypeBlack}, + {"LANGUAGECHANGE", attributeTypeBlack}, + {"LEAVEPICTUREINPICTURE", attributeTypeBlack}, + {"LEVELCHANGE", attributeTypeBlack}, + {"LOAD", attributeTypeBlack}, + {"LOADEDDATA", attributeTypeBlack}, + {"LOADEDMETADATA", attributeTypeBlack}, + {"LOADEND", attributeTypeBlack}, + {"LOADING", attributeTypeBlack}, + {"LOADINGDONE", attributeTypeBlack}, + {"LOADINGERROR", attributeTypeBlack}, + {"LOADSTART", attributeTypeBlack}, + {"LOSTPOINTERCAPTURE", attributeTypeBlack}, + {"MARK", attributeTypeBlack}, + {"MERCHANTVALIDATION", attributeTypeBlack}, + {"MESSAGE", attributeTypeBlack}, + {"MESSAGEERROR", attributeTypeBlack}, + {"MOUSEDOWN", attributeTypeBlack}, + {"MOUSEENTER", attributeTypeBlack}, + {"MOUSELEAVE", attributeTypeBlack}, + {"MOUSEMOVE", attributeTypeBlack}, + {"MOUSEOUT", attributeTypeBlack}, + {"MOUSEOVER", attributeTypeBlack}, + {"MOUSEUP", attributeTypeBlack}, + {"MOUSEWHEEL", attributeTypeBlack}, + {"MUTE", attributeTypeBlack}, + {"NAVIGATE", attributeTypeBlack}, + {"NAVIGATEERROR", attributeTypeBlack}, + {"NAVIGATESUCCESS", attributeTypeBlack}, + {"NEGOTIATIONNEEDED", attributeTypeBlack}, + {"NEXTTRACK", attributeTypeBlack}, + {"NOMATCH", attributeTypeBlack}, + {"NOTIFICATIONCLICK", attributeTypeBlack}, + {"NOTIFICATIONCLOSE", attributeTypeBlack}, + {"NOUPDATE", attributeTypeBlack}, + {"OBSOLETE", attributeTypeBlack}, + {"OFFLINE", attributeTypeBlack}, + {"ONLINE", attributeTypeBlack}, + {"OPEN", attributeTypeBlack}, + {"ORIENTATIONCHANGE", attributeTypeBlack}, + {"OVERFLOWCHANGED", attributeTypeBlack}, + {"PAGEHIDE", attributeTypeBlack}, + {"PAGESHOW", attributeTypeBlack}, + {"PASTE", attributeTypeBlack}, + {"PAUSE", attributeTypeBlack}, + {"PAYERDETAILCHANGE", attributeTypeBlack}, + {"PAYMENTAUTHORIZED", attributeTypeBlack}, + {"PAYMENTMETHODCHANGE", attributeTypeBlack}, + {"PAYMENTMETHODSELECTED", attributeTypeBlack}, + {"PLAY", attributeTypeBlack}, + {"PLAYING", attributeTypeBlack}, + {"POINTERCANCEL", attributeTypeBlack}, + {"POINTERDOWN", attributeTypeBlack}, + {"POINTERENTER", attributeTypeBlack}, + {"POINTERLEAVE", attributeTypeBlack}, + {"POINTERLOCKCHANGE", attributeTypeBlack}, + {"POINTERLOCKERROR", attributeTypeBlack}, + {"POINTERMOVE", attributeTypeBlack}, + {"POINTEROUT", attributeTypeBlack}, + {"POINTEROVER", attributeTypeBlack}, + {"POINTERUP", attributeTypeBlack}, + {"POPSTATE", attributeTypeBlack}, + {"PREVIOUSTRACK", attributeTypeBlack}, + {"PROCESSORERROR", attributeTypeBlack}, + {"PROGRESS", attributeTypeBlack}, + {"PUSH", attributeTypeBlack}, + {"PUSHNOTIFICATION", attributeTypeBlack}, + {"PUSHSUBSCRIPTIONCHANGE", attributeTypeBlack}, + {"QUALITYCHANGE", attributeTypeBlack}, + {"RATECHANGE", attributeTypeBlack}, + {"READYSTATECHANGE", attributeTypeBlack}, + {"REJECTIONHANDLED", attributeTypeBlack}, + {"RELEASE", attributeTypeBlack}, + {"REMOVE", attributeTypeBlack}, + {"REMOVESOURCEBUFFER", attributeTypeBlack}, + {"REMOVESTREAM", attributeTypeBlack}, + {"REMOVETRACK", attributeTypeBlack}, + {"RESET", attributeTypeBlack}, + {"RESIZE", attributeTypeBlack}, + {"RESOURCETIMINGBUFFERFULL", attributeTypeBlack}, + {"RESULT", attributeTypeBlack}, + {"RESUME", attributeTypeBlack}, + {"RTCTRANSFORM", attributeTypeBlack}, + {"SCROLL", attributeTypeBlack}, + {"SEARCH", attributeTypeBlack}, + {"SECURITYPOLICYVIOLATION", attributeTypeBlack}, + {"SEEKED", attributeTypeBlack}, + {"SEEKING", attributeTypeBlack}, + {"SELECT", attributeTypeBlack}, + {"SELECTEDCANDIDATEPAIRCHANGE", attributeTypeBlack}, + {"SELECTEND", attributeTypeBlack}, + {"SELECTIONCHANGE", attributeTypeBlack}, + {"SELECTSTART", attributeTypeBlack}, + {"SHIPPINGADDRESSCHANGE", attributeTypeBlack}, + {"SHIPPINGCONTACTSELECTED", attributeTypeBlack}, + {"SHIPPINGMETHODSELECTED", attributeTypeBlack}, + {"SHIPPINGOPTIONCHANGE", attributeTypeBlack}, + {"SHOW", attributeTypeBlack}, + {"SIGNALINGSTATECHANGE", attributeTypeBlack}, + {"SLOTCHANGE", attributeTypeBlack}, + {"SOUNDEND", attributeTypeBlack}, + {"SOUNDSTART", attributeTypeBlack}, + {"SOURCECLOSE", attributeTypeBlack}, + {"SOURCEENDED", attributeTypeBlack}, + {"SOURCEOPEN", attributeTypeBlack}, + {"SPEECHEND", attributeTypeBlack}, + {"SPEECHSTART", attributeTypeBlack}, + {"SQUEEZE", attributeTypeBlack}, + {"SQUEEZEEND", attributeTypeBlack}, + {"SQUEEZESTART", attributeTypeBlack}, + {"STALLED", attributeTypeBlack}, + {"START", attributeTypeBlack}, + {"STARTED", attributeTypeBlack}, + {"STARTSTREAMING", attributeTypeBlack}, + {"STATECHANGE", attributeTypeBlack}, + {"STOP", attributeTypeBlack}, + {"STORAGE", attributeTypeBlack}, + {"SUBMIT", attributeTypeBlack}, + {"SUCCESS", attributeTypeBlack}, + {"SUSPEND", attributeTypeBlack}, + {"TEXTINPUT", attributeTypeBlack}, + {"TIMEOUT", attributeTypeBlack}, + {"TIMEUPDATE", attributeTypeBlack}, + {"TOGGLE", attributeTypeBlack}, + {"TONECHANGE", attributeTypeBlack}, + {"TOUCHCANCEL", attributeTypeBlack}, + {"TOUCHEND", attributeTypeBlack}, + {"TOUCHFORCECHANGE", attributeTypeBlack}, + {"TOUCHMOVE", attributeTypeBlack}, + {"TOUCHSTART", attributeTypeBlack}, + {"TRACK", attributeTypeBlack}, + {"TRANSITIONCANCEL", attributeTypeBlack}, + {"TRANSITIONEND", attributeTypeBlack}, + {"TRANSITIONRUN", attributeTypeBlack}, + {"TRANSITIONSTART", attributeTypeBlack}, + {"UNCAPTUREDERROR", attributeTypeBlack}, + {"UNHANDLEDREJECTION", attributeTypeBlack}, + {"UNLOAD", attributeTypeBlack}, + {"UNMUTE", attributeTypeBlack}, + {"UPDATE", attributeTypeBlack}, + {"UPDATEEND", attributeTypeBlack}, + {"UPDATEFOUND", attributeTypeBlack}, + {"UPDATEREADY", attributeTypeBlack}, + {"UPDATESTART", attributeTypeBlack}, + {"UPGRADENEEDED", attributeTypeBlack}, + {"VALIDATEMERCHANT", attributeTypeBlack}, + {"VERSIONCHANGE", attributeTypeBlack}, + {"VISIBILITYCHANGE", attributeTypeBlack}, + {"VOICESCHANGED", attributeTypeBlack}, + {"VOLUMECHANGE", attributeTypeBlack}, + {"WAITING", attributeTypeBlack}, + {"WAITINGFORKEY", attributeTypeBlack}, + {"WEBGLCONTEXTCREATIONERROR", attributeTypeBlack}, + {"WEBGLCONTEXTLOST", attributeTypeBlack}, + {"WEBGLCONTEXTRESTORED", attributeTypeBlack}, + {"WEBKITANIMATIONEND", attributeTypeBlack}, + {"WEBKITANIMATIONITERATION", attributeTypeBlack}, + {"WEBKITANIMATIONSTART", attributeTypeBlack}, + {"WEBKITBEFORETEXTINSERTED", attributeTypeBlack}, + {"WEBKITBEGINFULLSCREEN", attributeTypeBlack}, + {"WEBKITCURRENTPLAYBACKTARGETISWIRELESSCHANGED", attributeTypeBlack}, + {"WEBKITENDFULLSCREEN", attributeTypeBlack}, + {"WEBKITFULLSCREENCHANGE", attributeTypeBlack}, + {"WEBKITFULLSCREENERROR", attributeTypeBlack}, + {"WEBKITKEYADDED", attributeTypeBlack}, + {"WEBKITKEYERROR", attributeTypeBlack}, + {"WEBKITKEYMESSAGE", attributeTypeBlack}, + {"WEBKITMOUSEFORCECHANGED", attributeTypeBlack}, + {"WEBKITMOUSEFORCEDOWN", attributeTypeBlack}, + {"WEBKITMOUSEFORCEUP", attributeTypeBlack}, + {"WEBKITMOUSEFORCEWILLBEGIN", attributeTypeBlack}, + {"WEBKITNEEDKEY", attributeTypeBlack}, + {"WEBKITNETWORKINFOCHANGE", attributeTypeBlack}, + {"WEBKITPLAYBACKTARGETAVAILABILITYCHANGED", attributeTypeBlack}, + {"WEBKITPRESENTATIONMODECHANGED", attributeTypeBlack}, + {"WEBKITREMOVESOURCEBUFFER", attributeTypeBlack}, + {"WEBKITSOURCECLOSE", attributeTypeBlack}, + {"WEBKITSOURCEENDED", attributeTypeBlack}, + {"WEBKITSOURCEOPEN", attributeTypeBlack}, + {"WEBKITTRANSITIONEND", attributeTypeBlack}, + {"WHEEL", attributeTypeBlack}, + {"WRITE", attributeTypeBlack}, + {"WRITEEND", attributeTypeBlack}, + {"WRITESTART", attributeTypeBlack}, + {"ZOOM", attributeTypeBlack}, +} + var blackTags = []string{ "APPLET", "BASE", diff --git a/xss_helpers.go b/xss_helpers.go index debf4a3..89527fc 100644 --- a/xss_helpers.go +++ b/xss_helpers.go @@ -35,22 +35,26 @@ func isBlackAttr(s string) int { return attributeTypeNone } - upperS := strings.ToUpper(strings.ReplaceAll(s, "\x00", "")) + sUpperWithoutNulls := strings.ToUpper(strings.ReplaceAll(s, "\x00", "")) if length >= 5 { - // javascript on.* - if strings.ToUpper(s[:2]) == "ON" { - // got javascript on- attribute name - return attributeTypeBlack - } - - if upperS == "XMLNS" || upperS == "XLINK" { + if sUpperWithoutNulls == "XMLNS" || sUpperWithoutNulls == "XLINK" { // got xmlns or xlink tags return attributeTypeBlack } + // JavaScript on.* event handlers + if sUpperWithoutNulls[:2] == "ON" { + eventName := sUpperWithoutNulls[2:] + // got javascript on- attribute name + for _, event := range blackEvents { + if eventName == event.name { + return event.attributeType + } + } + } } for _, black := range blacks { - if upperS == black.name { + if sUpperWithoutNulls == black.name { // got banner attribute name return black.attributeType } diff --git a/xss_test.go b/xss_test.go index 3fd4fbd..113f050 100644 --- a/xss_test.go +++ b/xss_test.go @@ -8,34 +8,47 @@ import ( "testing" ) +// Examples can be read at https://portswigger.net/web-security/cross-site-scripting/cheat-sheet func TestIsXSS(t *testing.T) { - examples := []string{ - "", - ">", - "x >", - "' >", - "\">", - "red;", - "red;}", - "red;\"/>", - "');}", - "onerror=alert(1)>", - "x onerror=alert(1);>", - "x' onerror=alert(1);>", - "x\" onerror=alert(1);>", - "", - "", - "", - "", - "", - "", + examples := []struct { + input string + isXSS bool + }{ + // True positives + {input: "", isXSS: true}, + {input: ">", isXSS: true}, + {input: "x >", isXSS: true}, + {input: "' >", isXSS: true}, + {input: "\">", isXSS: true}, + {input: "red;", isXSS: true}, + {input: "red;}", isXSS: true}, + {input: "red;\"/>", isXSS: true}, + {input: "');}", isXSS: true}, + {input: "onerror=alert(1)>", isXSS: true}, + {input: "x onerror=alert(1);>", isXSS: true}, + {input: "x' onerror=alert(1);>", isXSS: true}, + {input: "x\" onerror=alert(1);>", isXSS: true}, + {input: "", isXSS: true}, + {input: "", isXSS: true}, + {input: "", isXSS: true}, + {input: "", isXSS: true}, + {input: "", isXSS: true}, + {input: "", isXSS: true}, + {input: "", isXSS: true}, + {input: "<img title=\"\">", isXSS: true}, + {input: "javascript:/*-->", isXSS: true}, // polyglot payload + {input: "", isXSS: true}, + {input: "XSS", isXSS: true}, // Payload sample from https://github.com/payloadbox/xss-payload-list - "XSS\"\"\",\"XML namespace.\"),(\"\"\"<IMG SRC=\"javascript:javascript:alert(1)\">", + {input: "XSS\"\"\",\"XML namespace.\"),(\"\"\"<IMG SRC=\"javascript:javascript:alert(1)\">", isXSS: true}, + // True negatives + {input: "myvar=onfoobar==", isXSS: false}, + {input: "onY29va2llcw==", isXSS: false}, // base64 encoded "thisisacookie", prefixed by "on" } for _, example := range examples { - if !IsXSS(example) { - t.Errorf("[%s] is not XSS", example) + if res := IsXSS(example.input); res != example.isXSS { + t.Errorf("[%s] wanted: %t, got %t", example.input, example.isXSS, res) } } }