diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index a37b612746e78..c2eecdee7620d 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -53,12 +53,40 @@ NEW_ENUMERATE_HOOK_NAME = "_newEnumerate" ENUM_ENTRY_VARIABLE_NAME = "strings" INSTANCE_RESERVED_SLOTS = 1 +PREFERENCE_PREFIX = "tainting.source." + # This size is arbitrary. It is a power of 2 to make using it as a modulo # operand cheap, and is usually around 1/3-1/5th of the set size (sometimes # smaller for very large sets). GLOBAL_NAMES_PHF_SIZE = 256 +def format_preference_entry(taintSource): + return "%s%s" % (PREFERENCE_PREFIX, taintSource) + +def format_preference(taintSource): + return ( + "pref(\"%s\", true);" + % (format_preference_entry(taintSource)) + ) + +def maybe_add_preference(taintSource): + import pathlib + root = pathlib.Path(__file__).parent.parent.parent.resolve() + prefs = root / 'modules' / 'libpref' / 'init' / 'all.js' + prefs = prefs.resolve() + if(prefs.exists()): + preference_entry = format_preference_entry(taintSource) + with prefs.open(mode='r+') as pf: + if preference_entry in pf.read(): + print("%s already defined in %s... Skipping...\n" % (preference_entry, prefs)) + else: + pf.write("%s\n" % (format_preference(taintSource))) + print("Added preference '%s' to '%s'..\n" % (preference_entry, prefs)) + return True + else: + print("Preference file does not exist: %s!\n"% prefs) + return False def memberReservedSlot(member, descriptor): return ( @@ -7740,6 +7768,7 @@ def getWrapTemplateForType( exceptionCode, spiderMonkeyInterfacesAreStructs, isConstructorRetval=False, + taintSource=None ): """ Reflect a C++ value stored in "result", of IDL type "type" into JS. The @@ -7819,6 +7848,19 @@ def _setValue(value, wrapAsType=None, setter="set"): exceptionCode=exceptionCode, successCode=successCode, ) + # Attach taint metadata to the return value if it is a source + if taintSource is not None: + print("Generating taint source:", taintSource) + maybe_add_preference(taintSource) + taintHandler = dedent( + ( + """ + // Add taint source + MarkTaintSource(cx, ${jsvalRef}, "%s"); + """ + % (taintSource)) + ) + tail = taintHandler + tail return ("${jsvalRef}.%s(%s);\n" % (setter, value)) + tail def wrapAndSetPtr(wrapCall, failureCode=None): @@ -7828,16 +7870,32 @@ def wrapAndSetPtr(wrapCall, failureCode=None): """ if failureCode is None: failureCode = exceptionCode + + # TaintFox: create source code for tainting wrapped return value + markTaintSnippet = "" + if taintSource is not None: + print("Generating taint source for wrapped value:", taintSource) + maybe_add_preference(taintSource) + markTaintSnippet = dedent( + f""" + // Add taint source for wrapped value + MarkTaintSource(cx, args.rval(), "{taintSource}");""" + ) + return fill( """ if (!${wrapCall}) { $*{failureCode} } + + ${markTaintSnippet} + $*{successCode} """, wrapCall=wrapCall, failureCode=failureCode, successCode=successCode, + markTaintSnippet=markTaintSnippet, ) if type is None or type.isUndefined(): @@ -8259,7 +8317,7 @@ def wrapAndSetPtr(wrapCall, failureCode=None): raise TypeError("Need to learn to wrap primitive: %s" % type) -def wrapForType(type, descriptorProvider, templateValues): +def wrapForType(type, descriptorProvider, templateValues, taintSource = None): """ Reflect a C++ value of IDL type "type" into JS. TemplateValues is a dict that should contain: @@ -8302,6 +8360,7 @@ def wrapForType(type, descriptorProvider, templateValues): templateValues.get("exceptionCode", "return false;\n"), templateValues.get("spiderMonkeyInterfacesAreStructs", False), isConstructorRetval=templateValues.get("isConstructorRetval", False), + taintSource=taintSource )[0] defaultValues = {"obj": "obj"} @@ -9042,6 +9101,10 @@ def __init__( self.setSlot = ( not dontSetSlot and idlNode.isAttr() and idlNode.slotIndices is not None ) + + # Taintfox: create a label for the taint source + self.taintSource = GetLabelForErrorReporting(descriptor, idlNode, isConstructor) if memberIsTaintSource(self.idlNode) else None + cgThings = [] deprecated = idlNode.getExtendedAttribute("Deprecated") or ( @@ -9542,7 +9605,7 @@ def wrap_return_value(self): "obj": "conversionScope" if self.setSlot else "obj", } - wrapCode += wrapForType(self.returnType, self.descriptor, resultTemplateValues) + wrapCode += wrapForType(self.returnType, self.descriptor, resultTemplateValues, self.taintSource) if self.setSlot: if self.idlNode.isStatic(): @@ -11015,6 +11078,11 @@ def __init__( errorReportingLabel=None, additionalArg=None, ): + + # TaintFox: Check if return value should de marked as taint source and + # get name for taint source if needed + self.taintSource = errorReportingLabel if memberIsTaintSource(attr) else None + self.nativeName = nativeName self.errorReportingLabel = errorReportingLabel self.additionalArgs = [] if additionalArg is None else [additionalArg] @@ -11147,6 +11215,18 @@ def definition_body(self): slotIndex=memberReservedSlot(self.attr, self.descriptor), ) + # TaintFox: create source code for tainting cached return value + markTaintSnippet = "" + if self.taintSource is not None: + print("Generating taint source for cached value:", self.taintSource) + maybe_add_preference(self.taintSource) + markTaintSnippet = dedent( + f""" + // Add taint source for cached value + MarkTaintSource(cx, args.rval(), "{self.taintSource}");""" + ) + + prefix += fill( """ MOZ_ASSERT(JSCLASS_RESERVED_SLOTS(JS::GetClass(slotStorage)) > slotIndex); @@ -11155,6 +11235,9 @@ def definition_body(self): JS::Value cachedVal = JS::GetReservedSlot(slotStorage, slotIndex); if (!cachedVal.isUndefined()) { args.rval().set(cachedVal); + + ${markTaintSnippet} + // The cached value is in the compartment of slotStorage, // so wrap into the caller compartment as needed. return ${maybeWrap}(cx, args.rval()); @@ -11162,6 +11245,7 @@ def definition_body(self): } """, + markTaintSnippet=markTaintSnippet, maybeWrap=getMaybeWrapValueFuncForType(self.attr.type), ) @@ -11743,6 +11827,9 @@ def error_reporting_label(self): def memberReturnsNewObject(member): return member.getExtendedAttribute("NewObject") is not None +def memberIsTaintSource(member): + # Taintfox: check if this function is marked as a taint source: + return member.getExtendedAttribute("TaintSource") is not None class CGMemberJITInfo(CGThing): """ @@ -19035,9 +19122,19 @@ def descriptorClearsPropsInSlots(descriptor): for m in descriptor.interface.members ) + def hasAtLeastOneTaintsource(descriptor): + return any( + (m.isAttr() or m.isMethod()) and m.getExtendedAttribute("TaintSource") + for m in descriptor.interface.members + ) + bindingHeaders["nsJSUtils.h"] = any( descriptorClearsPropsInSlots(d) for d in descriptors ) + + bindingHeaders["nsTaintingUtils.h"] = any( + hasAtLeastOneTaintsource(d) for d in descriptors + ) # Make sure we can sanely use binding_detail in generated code. cgthings = [ diff --git a/dom/bindings/parser/WebIDL.py b/dom/bindings/parser/WebIDL.py index fb524bc78a3d3..fce170d8f8bcf 100644 --- a/dom/bindings/parser/WebIDL.py +++ b/dom/bindings/parser/WebIDL.py @@ -5722,6 +5722,7 @@ def handleExtendedAttribute(self, attr): or identifier == "BinaryName" or identifier == "NonEnumerable" or identifier == "BindingTemplate" + or identifier == "TaintSource" # Taintfox added ): # Known attributes that we don't need to do anything with here pass @@ -6743,6 +6744,7 @@ def handleExtendedAttribute(self, attr): or identifier == "NonEnumerable" or identifier == "Unexposed" or identifier == "WebExtensionStub" + or identifier == "TaintSource" # Taintfox added ): # Known attributes that we don't need to do anything with here pass diff --git a/dom/webidl/AudioContext.webidl b/dom/webidl/AudioContext.webidl index 804cb127a7895..df4447386570e 100644 --- a/dom/webidl/AudioContext.webidl +++ b/dom/webidl/AudioContext.webidl @@ -25,7 +25,9 @@ interface AudioContext : BaseAudioContext { [Throws] constructor(optional AudioContextOptions contextOptions = {}); + [TaintSource] readonly attribute double baseLatency; + [TaintSource] readonly attribute double outputLatency; AudioTimestamp getOutputTimestamp(); diff --git a/dom/webidl/AudioDestinationNode.webidl b/dom/webidl/AudioDestinationNode.webidl index 1e27b141ad89c..47416a6330b14 100644 --- a/dom/webidl/AudioDestinationNode.webidl +++ b/dom/webidl/AudioDestinationNode.webidl @@ -14,6 +14,7 @@ Exposed=Window] interface AudioDestinationNode : AudioNode { + [TaintSource] readonly attribute unsigned long maxChannelCount; }; diff --git a/dom/webidl/AudioNode.webidl b/dom/webidl/AudioNode.webidl index fbeed0e0b6cbd..37cafb80031ff 100644 --- a/dom/webidl/AudioNode.webidl +++ b/dom/webidl/AudioNode.webidl @@ -51,11 +51,13 @@ interface AudioNode : EventTarget { undefined disconnect(AudioParam destination, unsigned long output); readonly attribute BaseAudioContext context; + [TaintSource] readonly attribute unsigned long numberOfInputs; + [TaintSource] readonly attribute unsigned long numberOfOutputs; // Channel up-mixing and down-mixing rules for all inputs. - [SetterThrows] + [SetterThrows, TaintSource] attribute unsigned long channelCount; [SetterThrows, BinaryName="channelCountModeValue"] attribute ChannelCountMode channelCountMode; diff --git a/dom/webidl/BaseAudioContext.webidl b/dom/webidl/BaseAudioContext.webidl index 7bdd15ddfc588..5ccf810ba7d88 100644 --- a/dom/webidl/BaseAudioContext.webidl +++ b/dom/webidl/BaseAudioContext.webidl @@ -22,7 +22,9 @@ enum AudioContextState { [Exposed=Window] interface BaseAudioContext : EventTarget { readonly attribute AudioDestinationNode destination; + [TaintSource] readonly attribute float sampleRate; + [TaintSource] readonly attribute double currentTime; readonly attribute AudioListener listener; readonly attribute AudioContextState state; diff --git a/dom/webidl/HTMLCanvasElement.webidl b/dom/webidl/HTMLCanvasElement.webidl index dff2ac29895dc..4afe31f4da30c 100644 --- a/dom/webidl/HTMLCanvasElement.webidl +++ b/dom/webidl/HTMLCanvasElement.webidl @@ -26,7 +26,7 @@ interface HTMLCanvasElement : HTMLElement { [Throws] nsISupports? getContext(DOMString contextId, optional any contextOptions = null); - [Throws, NeedsSubjectPrincipal] + [Throws, NeedsSubjectPrincipal, TaintSource] DOMString toDataURL(optional DOMString type = "", optional any encoderOptions); [Throws, NeedsSubjectPrincipal] diff --git a/dom/webidl/HTMLElement.webidl b/dom/webidl/HTMLElement.webidl index 2f467438ce81b..a457059b5411d 100644 --- a/dom/webidl/HTMLElement.webidl +++ b/dom/webidl/HTMLElement.webidl @@ -89,7 +89,9 @@ partial interface HTMLElement { readonly attribute Element? offsetParent; readonly attribute long offsetTop; readonly attribute long offsetLeft; + [TaintSource] readonly attribute long offsetWidth; + [TaintSource] readonly attribute long offsetHeight; }; diff --git a/dom/webidl/History.webidl b/dom/webidl/History.webidl index 934214965dca4..879ada6a1a4ce 100644 --- a/dom/webidl/History.webidl +++ b/dom/webidl/History.webidl @@ -15,7 +15,7 @@ enum ScrollRestoration { "auto", "manual" }; [Exposed=Window] interface History { - [Throws] + [Throws, TaintSource] readonly attribute unsigned long length; [Throws] attribute ScrollRestoration scrollRestoration; diff --git a/dom/webidl/MediaDeviceInfo.webidl b/dom/webidl/MediaDeviceInfo.webidl index 7bceadc22eb1a..7968a65702048 100644 --- a/dom/webidl/MediaDeviceInfo.webidl +++ b/dom/webidl/MediaDeviceInfo.webidl @@ -16,9 +16,13 @@ enum MediaDeviceKind { [Func="Navigator::HasUserMediaSupport", Exposed=Window] interface MediaDeviceInfo { + [TaintSource] readonly attribute DOMString deviceId; + [TaintSource] readonly attribute MediaDeviceKind kind; + [TaintSource] readonly attribute DOMString label; + [TaintSource] readonly attribute DOMString groupId; [Default] object toJSON(); diff --git a/dom/webidl/MimeType.webidl b/dom/webidl/MimeType.webidl index 86663bba12ff1..d124920c3523d 100644 --- a/dom/webidl/MimeType.webidl +++ b/dom/webidl/MimeType.webidl @@ -6,8 +6,11 @@ [Exposed=Window] interface MimeType { + [TaintSource] readonly attribute DOMString type; + [TaintSource] readonly attribute DOMString description; + [TaintSource] readonly attribute DOMString suffixes; readonly attribute Plugin enabledPlugin; }; diff --git a/dom/webidl/Navigator.webidl b/dom/webidl/Navigator.webidl index 993d9504dd81d..53d6c05b39895 100644 --- a/dom/webidl/Navigator.webidl +++ b/dom/webidl/Navigator.webidl @@ -51,17 +51,17 @@ Navigator includes GlobalPrivacyControl; interface mixin NavigatorID { // WebKit/Blink/Trident/Presto support this (hardcoded "Mozilla"). - [Constant, Cached, Throws] + [Constant, Cached, Throws, TaintSource] readonly attribute DOMString appCodeName; // constant "Mozilla" - [Constant, Cached] + [Constant, Cached, TaintSource] readonly attribute DOMString appName; // constant "Netscape" - [Constant, Cached, Throws, NeedsCallerType] + [Constant, Cached, Throws, NeedsCallerType, TaintSource] readonly attribute DOMString appVersion; - [Pure, Cached, Throws, NeedsCallerType] + [Pure, Cached, Throws, NeedsCallerType, TaintSource] readonly attribute DOMString platform; - [Pure, Cached, Throws, NeedsCallerType] + [Pure, Cached, Throws, NeedsCallerType, TaintSource] readonly attribute DOMString userAgent; - [Constant, Cached] + [Constant, Cached, TaintSource] readonly attribute DOMString product; // constant "Gecko" // Everyone but WebKit/Blink supports this. See bug 679971. @@ -76,9 +76,9 @@ interface mixin NavigatorLanguage { // main-thread from the worker thread anytime we need to retrieve them. They // are updated when pref intl.accept_languages is changed. - [Pure, Cached] + [Pure, Cached, TaintSource] readonly attribute DOMString? language; - [Pure, Cached, Frozen] + [Pure, Cached, Frozen, TaintSource] readonly attribute sequence languages; }; @@ -121,6 +121,7 @@ partial interface Navigator { // http://www.w3.org/TR/tracking-dnt/ sort of partial interface Navigator { + [TaintSource] readonly attribute DOMString doNotTrack; }; @@ -154,7 +155,7 @@ partial interface Navigator { // http://www.w3.org/TR/pointerevents/#extensions-to-the-navigator-interface partial interface Navigator { - [NeedsCallerType] + [NeedsCallerType, TaintSource] readonly attribute long maxTouchPoints; }; @@ -179,17 +180,20 @@ partial interface Navigator { }; partial interface Navigator { - [Throws, Constant, Cached, NeedsCallerType] + [Throws, Constant, Cached, NeedsCallerType, TaintSource] readonly attribute DOMString oscpu; // WebKit/Blink support this; Trident/Presto do not. + [TaintSource] readonly attribute DOMString vendor; // WebKit/Blink supports this (hardcoded ""); Trident/Presto do not. + [TaintSource] readonly attribute DOMString vendorSub; // WebKit/Blink supports this (hardcoded "20030107"); Trident/Presto don't + [TaintSource] readonly attribute DOMString productSub; // WebKit/Blink/Trident/Presto support this. readonly attribute boolean cookieEnabled; - [Throws, Constant, Cached, NeedsCallerType] + [Throws, Constant, Cached, NeedsCallerType, TaintSource] readonly attribute DOMString buildID; // WebKit/Blink/Trident/Presto support this. @@ -292,6 +296,7 @@ partial interface Navigator { }; interface mixin NavigatorConcurrentHardware { + [TaintSource] readonly attribute unsigned long long hardwareConcurrency; }; diff --git a/dom/webidl/Plugin.webidl b/dom/webidl/Plugin.webidl index 8e66858d77313..d80a0fc0b390c 100644 --- a/dom/webidl/Plugin.webidl +++ b/dom/webidl/Plugin.webidl @@ -7,8 +7,11 @@ [LegacyUnenumerableNamedProperties, Exposed=Window] interface Plugin { + [TaintSource] readonly attribute DOMString description; + [TaintSource] readonly attribute DOMString filename; + [TaintSource] readonly attribute DOMString name; readonly attribute unsigned long length; diff --git a/dom/webidl/Screen.webidl b/dom/webidl/Screen.webidl index c929fe4b48ccd..32c7d0907d246 100644 --- a/dom/webidl/Screen.webidl +++ b/dom/webidl/Screen.webidl @@ -7,16 +7,24 @@ interface Screen : EventTarget { // CSSOM-View // http://dev.w3.org/csswg/cssom-view/#the-screen-interface + [TaintSource] readonly attribute long availWidth; + [TaintSource] readonly attribute long availHeight; + [TaintSource] readonly attribute long width; + [TaintSource] readonly attribute long height; + [TaintSource] readonly attribute long colorDepth; + [TaintSource] readonly attribute long pixelDepth; readonly attribute long top; readonly attribute long left; + [TaintSource] readonly attribute long availTop; + [TaintSource] readonly attribute long availLeft; /** diff --git a/dom/webidl/VisualViewport.webidl b/dom/webidl/VisualViewport.webidl index cacda509b6713..3f2b8996977c1 100644 --- a/dom/webidl/VisualViewport.webidl +++ b/dom/webidl/VisualViewport.webidl @@ -9,15 +9,22 @@ [Exposed=Window] interface VisualViewport : EventTarget { + [TaintSource] readonly attribute double offsetLeft; + [TaintSource] readonly attribute double offsetTop; + [TaintSource] readonly attribute double pageLeft; + [TaintSource] readonly attribute double pageTop; + [TaintSource] readonly attribute double width; + [TaintSource] readonly attribute double height; + [TaintSource] readonly attribute double scale; attribute EventHandler onresize; diff --git a/dom/webidl/WebGLRenderingContext.webidl b/dom/webidl/WebGLRenderingContext.webidl index 00585a06bf66b..537d3380f44d4 100644 --- a/dom/webidl/WebGLRenderingContext.webidl +++ b/dom/webidl/WebGLRenderingContext.webidl @@ -109,8 +109,11 @@ interface WebGLActiveInfo { [Exposed=(Window,Worker), Func="mozilla::dom::OffscreenCanvas::PrefEnabledOnWorkerThread"] interface WebGLShaderPrecisionFormat { + [TaintSource] readonly attribute GLint rangeMin; + [TaintSource] readonly attribute GLint rangeMax; + [TaintSource] readonly attribute GLint precision; }; @@ -631,7 +634,7 @@ interface mixin WebGLRenderingContextBase { [WebGLHandlesContextLoss] GLint getAttribLocation(WebGLProgram program, DOMString name); any getBufferParameter(GLenum target, GLenum pname); - [Throws] + [Throws, TaintSource] any getParameter(GLenum pname); [WebGLHandlesContextLoss] GLenum getError(); diff --git a/dom/webidl/Window.webidl b/dom/webidl/Window.webidl index 009ec26198601..f08cc51674503 100644 --- a/dom/webidl/Window.webidl +++ b/dom/webidl/Window.webidl @@ -328,8 +328,8 @@ partial interface Window { [Throws, NeedsCallerType] undefined resizeBy(long x, long y); // viewport - [Replaceable, Throws] readonly attribute double innerWidth; - [Replaceable, Throws] readonly attribute double innerHeight; + [Replaceable, Throws, TaintSource] readonly attribute double innerWidth; + [Replaceable, Throws, TaintSource] readonly attribute double innerHeight; // viewport scrolling undefined scroll(unrestricted double x, unrestricted double y); @@ -345,20 +345,20 @@ partial interface Window { [ChromeOnly] undefined mozScrollSnap(); // The four properties below are double per spec at the moment, but whether // that will continue is unclear. - [Replaceable, Throws] readonly attribute double scrollX; - [Replaceable, Throws] readonly attribute double pageXOffset; - [Replaceable, Throws] readonly attribute double scrollY; - [Replaceable, Throws] readonly attribute double pageYOffset; + [Replaceable, Throws, TaintSource] readonly attribute double scrollX; + [Replaceable, Throws, TaintSource] readonly attribute double pageXOffset; + [Replaceable, Throws, TaintSource] readonly attribute double scrollY; + [Replaceable, Throws, TaintSource] readonly attribute double pageYOffset; // Aliases for screenX / screenY. - [Replaceable, Throws, NeedsCallerType] readonly attribute double screenLeft; - [Replaceable, Throws, NeedsCallerType] readonly attribute double screenTop; + [Replaceable, Throws, NeedsCallerType, TaintSource] readonly attribute double screenLeft; + [Replaceable, Throws, NeedsCallerType, TaintSource] readonly attribute double screenTop; // client - [Replaceable, Throws, NeedsCallerType] readonly attribute double screenX; - [Replaceable, Throws, NeedsCallerType] readonly attribute double screenY; - [Replaceable, Throws, NeedsCallerType] readonly attribute double outerWidth; - [Replaceable, Throws, NeedsCallerType] readonly attribute double outerHeight; + [Replaceable, Throws, NeedsCallerType, TaintSource] readonly attribute double screenX; + [Replaceable, Throws, NeedsCallerType, TaintSource] readonly attribute double screenY; + [Replaceable, Throws, NeedsCallerType, TaintSource] readonly attribute double outerWidth; + [Replaceable, Throws, NeedsCallerType, TaintSource] readonly attribute double outerHeight; }; // https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animation-frames @@ -429,7 +429,7 @@ partial interface Window { readonly attribute float mozInnerScreenX; [Throws, NeedsCallerType] readonly attribute float mozInnerScreenY; - [Replaceable, Throws, NeedsCallerType] + [Replaceable, Throws, NeedsCallerType, TaintSource] readonly attribute double devicePixelRatio; // Allows chrome code to convert desktop pixels to device pixels and vice diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp index 840f76593e01b..e7cfaaeb5c021 100644 --- a/js/src/builtin/Array.cpp +++ b/js/src/builtin/Array.cpp @@ -41,6 +41,7 @@ #include "vm/JSContext.h" #include "vm/JSFunction.h" #include "vm/JSObject.h" +#include "vm/NumberObject.h" #include "vm/PlainObject.h" // js::PlainObject #include "vm/SelfHosting.h" #include "vm/Shape.h" @@ -1372,6 +1373,7 @@ bool js::array_join(JSContext* cx, unsigned argc, Value* vp) { return false; } + if(str->isTainted()) { // TaintFox: add taint operation. str->taint().extend(TaintOperationFromContext(cx, "Array.join", true, sepstr)); @@ -4086,7 +4088,6 @@ enum class SearchKind { // semantics are used. Includes, }; - template static bool SearchElementDense(JSContext* cx, HandleValue val, Iter iterator, MutableHandleValue rval) { @@ -4117,8 +4118,15 @@ static bool SearchElementDense(JSContext* cx, HandleValue val, Iter iterator, } // Fast path for numbers. - if (val.isNumber()) { - double dval = val.toNumber(); + if (val.isNumber() || isTaintedNumber(val)) { + double dval; + if(isTaintedNumber(val)) { + if (!ToNumber(cx, val, &dval)) { + return false; + } + } else { + dval = val.toNumber(); + } // For |includes|, two NaN values are considered equal, so we use a // different implementation for NaN. if (Kind == SearchKind::Includes && std::isnan(dval)) { @@ -4128,8 +4136,15 @@ static bool SearchElementDense(JSContext* cx, HandleValue val, Iter iterator, }; return iterator(cx, cmp, rval); } - auto cmp = [dval](JSContext*, const Value& element, bool* equal) { - *equal = (element.isNumber() && element.toNumber() == dval); + auto cmp = [dval](JSContext* context, const Value& element, bool* equal) { + if(isTaintedNumber(element)) { + NumberObject* obj = &element.toObject().as(); + double x = obj->unbox();; + + *equal = x == dval; + } else { + *equal = (element.isNumber() && element.toNumber() == dval); + } return true; }; return iterator(cx, cmp, rval); diff --git a/js/src/builtin/JSON.cpp b/js/src/builtin/JSON.cpp index d996ee41bff30..49da30340d7f5 100644 --- a/js/src/builtin/JSON.cpp +++ b/js/src/builtin/JSON.cpp @@ -413,6 +413,14 @@ static bool PreprocessValue(JSContext* cx, HandleObject holder, KeyType key, } if (cls == ESClass::Number) { + + // TaintFox: Abort, if object is a tainted number, to prevent taint loss. + // special handling for tainted numbers at later stage (method Str()) + if (isTaintedNumber(vp)){ + return true; + } + + double d; if (!ToNumber(cx, vp, &d)) { return false; @@ -797,6 +805,14 @@ static bool SerializeJSONProperty(JSContext* cx, const Value& v, return v.toBoolean() ? scx->sb.append("true") : scx->sb.append("false"); } + + // TaintFox: Convert tainted number to string (internally propagates taint) + // and append to string builder + if (isTaintedNumber(v)){ + HandleValue hv = HandleValue::fromMarkedLocation(&v); + return scx->sb.append(JS::ToString(cx, hv)); + } + /* Step 9. */ if (v.isNumber()) { if (v.isDouble()) { diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp index 4dd5bfa30e42b..68efa3da24175 100644 --- a/js/src/builtin/MapObject.cpp +++ b/js/src/builtin/MapObject.cpp @@ -82,6 +82,17 @@ bool HashableValue::setValue(JSContext* cx, HandleValue v) { value = v; } + // Additional check to unbox if we have a tainted Number + // Required to fix breakage with playwright integration + // But only if tainted as default JavaScript behaviour is that + // primitive and NumberObject keys are distinct. + if (value.isObject() && value.toObject().is()) { + NumberObject& number = value.toObject().as(); + if (number.taint()) { + value = NumberValue(number.unbox()); + } + } + MOZ_ASSERT(value.isUndefined() || value.isNull() || value.isBoolean() || value.isNumber() || value.isString() || value.isSymbol() || value.isObject() || value.isBigInt() || diff --git a/js/src/builtin/Number.js b/js/src/builtin/Number.js index 2291033286ddc..73293fd0abb39 100644 --- a/js/src/builtin/Number.js +++ b/js/src/builtin/Number.js @@ -41,23 +41,30 @@ function Number_toLocaleString() { // ES6 draft ES6 20.1.2.4 function Number_isFinite(num) { - if (typeof num !== "number") { - return false; - } + // TaintFox: get primitive value, if value is tainted number object + num = GetNumberObjectValueIfTainted(num); + + if (typeof num !== "number") + return false; return num - num === 0; } // ES6 draft ES6 20.1.2.2 function Number_isNaN(num) { - if (typeof num !== "number") { - return false; - } + // TaintFox: get primitive value, if value is tainted number object + num = GetNumberObjectValueIfTainted(num); + + if (typeof num !== "number") + return false; return num !== num; } // ES2021 draft rev 889f2f30cf554b7ed812c0984626db1c8a4997c7 // 20.1.2.3 Number.isInteger ( number ) function Number_isInteger(number) { + // TaintFox: get primitive value, if value is tainted number object + number = GetNumberObjectValueIfTainted(number); + // Step 1. (Inlined call to IsIntegralNumber) // 7.2.6 IsIntegralNumber, step 1. @@ -76,6 +83,9 @@ function Number_isInteger(number) { // ES2021 draft rev 889f2f30cf554b7ed812c0984626db1c8a4997c7 // 20.1.2.5 Number.isSafeInteger ( number ) function Number_isSafeInteger(number) { + // TaintFox: get primitive value, if value is tainted number object + number = GetNumberObjectValueIfTainted(number); + // Step 1. (Inlined call to IsIntegralNumber) // 7.2.6 IsIntegralNumber, step 1. diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp index 0e9afb9a116f6..79e0189ceb166 100644 --- a/js/src/builtin/String.cpp +++ b/js/src/builtin/String.cpp @@ -52,6 +52,7 @@ #include "vm/GlobalObject.h" #include "vm/JSContext.h" #include "vm/JSObject.h" +#include "vm/NumberObject.h" #include "vm/RegExpObject.h" #include "vm/SelfHosting.h" #include "vm/StaticStrings.h" @@ -60,6 +61,7 @@ #include "vm/GeckoProfiler-inl.h" #include "vm/InlineCharBuffer-inl.h" #include "vm/NativeObject-inl.h" +#include "vm/NumberObject-inl.h" #include "vm/StringObject-inl.h" #include "vm/StringType-inl.h" @@ -167,103 +169,15 @@ str_taint_getter(JSContext* cx, unsigned argc, Value* vp) if (!str) return false; - // Wrap all taint ranges of the string. - RootedValueVector ranges(cx); - for (const TaintRange& taint_range : str->taint()) { - RootedObject range(cx, JS_NewObject(cx, nullptr)); - if(!range) - return false; - - if (!JS_DefineProperty(cx, range, "begin", taint_range.begin(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || - !JS_DefineProperty(cx, range, "end", taint_range.end(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - // Wrap the taint flow for the current range. - RootedValueVector taint_flow(cx); - for (TaintNode& taint_node : taint_range.flow()) { - RootedObject node(cx, JS_NewObject(cx, nullptr)); - if (!node) - return false; - - RootedString operation(cx, JS_NewStringCopyZ(cx, taint_node.operation().name())); - if (!operation) - return false; - - if (!JS_DefineProperty(cx, node, "operation", operation, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - RootedValue isBuiltIn(cx); - isBuiltIn.setBoolean(taint_node.operation().is_native()); - - if (!JS_DefineProperty(cx, node, "builtin", isBuiltIn, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - RootedValue isSource(cx); - isSource.setBoolean(taint_node.operation().isSource()); - - if (!JS_DefineProperty(cx, node, "source", isSource, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - // Wrap the location - RootedObject location(cx, JS_NewObject(cx, nullptr)); - if (!location) - return false; - RootedString filename(cx, JS_NewUCStringCopyZ(cx, taint_node.operation().location().filename().c_str())); - if (!filename) - return false; - RootedString function(cx, JS_NewUCStringCopyZ(cx, taint_node.operation().location().function().c_str())); - if (!function) - return false; - // Also add the MD5 hash of the containing function - RootedString hash(cx, JS_NewStringCopyZ(cx, JS::convertDigestToHexString(taint_node.operation().location().scriptHash()).c_str())); - if (!hash) - return false; - - if (!JS_DefineProperty(cx, location, "filename", filename, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || - !JS_DefineProperty(cx, location, "function", function, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || - !JS_DefineProperty(cx, location, "line", taint_node.operation().location().line(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || - !JS_DefineProperty(cx, location, "pos", taint_node.operation().location().pos(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || - !JS_DefineProperty(cx, location, "scriptline", taint_node.operation().location().scriptStartLine(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || - !JS_DefineProperty(cx, location, "scripthash", hash, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - if (!JS_DefineProperty(cx, node, "location", location, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - // Wrap the arguments - RootedValueVector taint_arguments(cx); - for (auto& taint_argument : taint_node.operation().arguments()) { - RootedString argument(cx, JS_NewUCStringCopyZ(cx, taint_argument.c_str())); - if (!argument) - return false; - - if (!taint_arguments.append(StringValue(argument))) - return false; - } - - RootedObject arguments(cx, NewDenseCopiedArray(cx, taint_arguments.length(), taint_arguments.begin())); - if (!JS_DefineProperty(cx, node, "arguments", arguments, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - if (!taint_flow.append(ObjectValue(*node))) - return false; - } - - RootedObject flow(cx, NewDenseCopiedArray(cx, taint_flow.length(), taint_flow.begin())); - if (!flow) - return false; - if (!JS_DefineProperty(cx, range, "flow", flow, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) - return false; - - if (!ranges.append(ObjectValue(*range))) - return false; + RootedObject taint_obj(cx, JS_NewObject(cx, nullptr)); + if (!getStringTaintObject(cx, str->taint(), taint_obj)) { + return false; } - JSObject* array = NewDenseCopiedArray(cx, ranges.length(), ranges.begin()); - if (!array) + if (!JS_GetProperty(cx, taint_obj, "ranges", args.rval())) { return false; + } - args.rval().setObject(*array); return true; } @@ -297,9 +211,10 @@ construct_taint_flow(JSContext* cx, HandleObject flow_object, TaintNode** flow) // TODO process arguments as well UniqueChars op_str = JS_EncodeStringToUTF8(cx, operation); - *flow = new TaintNode(*flow, TaintOperation(op_str.get())); - if ((*flow)->parent()) - (*flow)->parent()->release(); + *flow = new TaintNode(TaintOperation(op_str.get())); + // Foxhound: Commented out for testing 2022-10-20 + // if ((*flow)->parent()) + // (*flow)->parent()->release(); } return true; @@ -2173,6 +2088,13 @@ static bool str_charAt(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); RootedString str(cx); size_t i; + + // TaintFox: try to get taint from parameter + SafeStringTaint parameterTaint; + if(args.length() != 0 && isTaintedNumber(args[0])){ + parameterTaint.set(0, getNumberTaint(args[0])); + } + if (args.thisv().isString() && args.length() != 0 && args[0].isInt32()) { str = args.thisv().toString(); i = size_t(args[0].toInt32()); @@ -2197,10 +2119,15 @@ static bool str_charAt(JSContext* cx, unsigned argc, Value* vp) { } // TaintFox: avoid atoms here if the base string is tainted. TODO(samuel) + // Taintfox: there is currently no branch for both the this-string and the + // parameter-number being tainted; should be added later once model is expanded str = NewDependentString(cx, str, i, 1); if (str->isTainted()) { str->taint().extend(TaintOperation("charAt", true, TaintLocationFromContext(cx), { taintarg(cx, i) })); + } else if(parameterTaint.hasTaint()) { + str->setTaint(cx,parameterTaint); } + // Taintfox: avoid creating atoms //str = cx->staticStrings().getUnitStringForElement(cx, str, i); @@ -2240,6 +2167,12 @@ bool js::str_charCodeAt_impl(JSContext* cx, HandleString string, return false; } res.setInt32(c); + + // TaintFox: Taint propagation for char codes of tainted strings via charCodeAt(). + if (string->taint().at(i)) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), string->taint().at(i))); + } + return true; out_of_range: @@ -4348,10 +4281,13 @@ bool js::str_fromCharCode(JSContext* cx, unsigned argc, Value* vp) { MOZ_ASSERT(args.length() <= ARGS_LENGTH_MAX); + // FoxHound: Disable optimization to simplify changes needed + /* // Optimize the single-char case. if (args.length() == 1) { return str_fromCharCode_one_arg(cx, args[0], args.rval()); } + */ // Optimize the case where the result will definitely fit in an inline // string (thin or fat) and so we don't need to malloc the chars. (We could @@ -4378,6 +4314,13 @@ bool js::str_fromCharCode(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox: loop at input args, scan for tainted numbers and propagate taint to string + for (unsigned i = 0; i < args.length(); i++) { + if (isTaintedNumber(args[i])) { + str->taint().set(i, getNumberTaint(args[i])); + } + } + args.rval().setString(str); return true; } diff --git a/js/src/gc/Nursery.cpp b/js/src/gc/Nursery.cpp index 4cef797491fd7..06feee580e8ef 100644 --- a/js/src/gc/Nursery.cpp +++ b/js/src/gc/Nursery.cpp @@ -32,6 +32,7 @@ #include "util/GetPidProvider.h" // getpid() #include "util/Poison.h" #include "vm/JSONPrinter.h" +#include "vm/NumberObject.h" #include "vm/Realm.h" #include "vm/Time.h" @@ -39,6 +40,7 @@ #include "gc/Marking-inl.h" #include "gc/StableCellHasher-inl.h" #include "vm/GeckoProfiler-inl.h" +#include "vm/NumberObject-inl.h" using namespace js; using namespace js::gc; @@ -1708,7 +1710,7 @@ void js::Nursery::sweep() { sweepMapAndSetObjects(); // Taintfox: clean up strings (mostly taint) - sweepStrings(); + sweepTaintableThings(); runtime()->caches().sweepAfterMinorGC(&trc); } @@ -2085,14 +2087,24 @@ void js::Nursery::sweepMapAndSetObjects() { SetObject::sweepAfterMinorGC(gcx, setobj); } setsWithNurseryMemory_.clearAndFree(); + } -void js::Nursery::sweepStrings() { +void js::Nursery::sweepTaintableThings() { auto* gcx = runtime()->gcContext(); + + // Strings for (auto* str : stringsWithNurseryMemory_) { JSString::sweepAfterMinorGC(gcx, str); } stringsWithNurseryMemory_.clearAndFree(); + + // Number Objects + for (auto* obj : numberObjectsWithNurseryMemory_) { + NumberObject::sweepAfterMinorGC(gcx, obj); + } + numberObjectsWithNurseryMemory_.clearAndFree(); + } void js::Nursery::joinDecommitTask() { decommitTask->join(); } diff --git a/js/src/gc/Nursery.h b/js/src/gc/Nursery.h index 758e63b897a8f..98b7884f2a6e7 100644 --- a/js/src/gc/Nursery.h +++ b/js/src/gc/Nursery.h @@ -61,6 +61,7 @@ struct NurseryChunk; class HeapSlot; class JSONPrinter; class MapObject; +class NumberObject; class SetObject; class JS_PUBLIC_API Sprinter; @@ -323,6 +324,12 @@ class Nursery { bool enableProfiling() const { return enableProfiling_; } + bool addNumberObjectWithNurseryMemory(NumberObject* num) { + MOZ_ASSERT_IF(!numberObjectsWithNurseryMemory_.empty(), + numberObjectsWithNurseryMemory_.back() != num); + return numberObjectsWithNurseryMemory_.append(num); + } + bool addStringWithNurseryMemory(JSString* str) { MOZ_ASSERT_IF(!stringsWithNurseryMemory_.empty(), stringsWithNurseryMemory_.back() != str); @@ -478,7 +485,7 @@ class Nursery { void sweepMapAndSetObjects(); // Taintfox: we also need to sweep strings to clean up taint information - void sweepStrings(); + void sweepTaintableThings(); // Allocate a buffer for a given zone, using the nursery if possible. void* allocateBuffer(JS::Zone* zone, size_t nbytes); @@ -634,6 +641,7 @@ class Nursery { Vector mapsWithNurseryMemory_; Vector setsWithNurseryMemory_; Vector stringsWithNurseryMemory_; + Vector numberObjectsWithNurseryMemory_; UniquePtr decommitTask; diff --git a/js/src/jit/Ion.h b/js/src/jit/Ion.h index 621575e59d196..0f672fe333f0b 100644 --- a/js/src/jit/Ion.h +++ b/js/src/jit/Ion.h @@ -77,6 +77,7 @@ void LinkIonScript(JSContext* cx, HandleScript calleescript); uint8_t* LazyLinkTopActivation(JSContext* cx, LazyLinkExitFrameLayout* frame); inline bool IsIonInlinableGetterOrSetterOp(JSOp op) { + // Taintfox: TODO: May need to disable Ion with primitive tainting // JSOp::GetProp, JSOp::CallProp, JSOp::Length, JSOp::GetElem, // and JSOp::CallElem. (Inlined Getters) // JSOp::SetProp, JSOp::SetName, JSOp::SetGName (Inlined Setters) diff --git a/js/src/jit/JitOptions.cpp b/js/src/jit/JitOptions.cpp index a8645ade1cc27..cd5674e179c24 100644 --- a/js/src/jit/JitOptions.cpp +++ b/js/src/jit/JitOptions.cpp @@ -127,7 +127,9 @@ DefaultJitOptions::DefaultJitOptions() { SET_DEFAULT(disableBailoutLoopCheck, false); // Whether the Baseline Interpreter is enabled. - SET_DEFAULT(baselineInterpreter, true); + // TaintFox: disable Baseline Interpreter because it is not compatible with the current + // number tainting functionality + SET_DEFAULT(baselineInterpreter, false); #ifdef ENABLE_PORTABLE_BASELINE_INTERP // Whether the Portable Baseline Interpreter is enabled. diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index e34017e639f5f..96984348be13b 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -4877,6 +4877,9 @@ JS_MarkTaintSource(JSContext* cx, JS::MutableHandleValue value, const TaintOpera if (value.isString()) { // If we have a string, set taint directly JS_MarkTaintSource(cx, value.toString(), op); + } else if (value.isNumber()) { + double d = value.toNumber(); + value.setObject(*NumberObject::createTainted(cx, d, TaintFlow(op))); } else if (value.isObject()) { // If it is an object, loop over contents // This function is used for convenience to taint all diff --git a/js/src/jsmath.cpp b/js/src/jsmath.cpp index 41a9911b33d89..bc567a483461a 100644 --- a/js/src/jsmath.cpp +++ b/js/src/jsmath.cpp @@ -27,6 +27,7 @@ #include "js/PropertySpec.h" #include "util/DifferentialTesting.h" #include "vm/JSContext.h" +#include "vm/NumberObject.h" #include "vm/Realm.h" #include "vm/Time.h" @@ -78,6 +79,15 @@ static bool math_function(JSContext* cx, CallArgs& args) { // NB: Always stored as a double so the math function can be inlined // through MMathFunction. double z = F(x); + + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, z, obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setDouble(z); return true; } @@ -97,6 +107,14 @@ bool js::math_abs(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, math_abs_impl(x), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(math_abs_impl(x)); return true; } @@ -150,6 +168,13 @@ static bool math_atan2(JSContext* cx, unsigned argc, Value* vp) { } double z = ecmaAtan2(y, x); + + // TaintFox: Taint propagation for math.atan2 + if (isAnyTaintedNumber(args[0], args[1])) { + args.rval().setObject(*NumberObject::createTainted(cx, z, getAnyNumberTaint(args[0], args[1], "Math.atan2"))); + return true; + } + args.rval().setDouble(z); return true; } @@ -172,6 +197,14 @@ static bool math_ceil(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, math_ceil_impl(x), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(math_ceil_impl(x)); return true; } @@ -189,6 +222,14 @@ static bool math_clz32(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, n == 0? 32: mozilla::CountLeadingZeroes32(n), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + if (n == 0) { args.rval().setInt32(32); return true; @@ -245,6 +286,14 @@ bool js::math_floor(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, math_floor_impl(x), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(math_floor_impl(x)); return true; } @@ -259,6 +308,12 @@ bool js::math_imul_handle(JSContext* cx, HandleValue lhs, HandleValue rhs, return false; } + // TaintFox: Taint propagation for math.imul. + if (isAnyTaintedNumber(lhs, rhs)) { + res.setObject(*NumberObject::createTainted(cx, WrappingMultiply(a, b), getAnyNumberTaint(lhs, rhs, "Math.imul"))); + return true; + } + res.setInt32(WrappingMultiply(a, b)); return true; } @@ -299,6 +354,19 @@ static bool math_fround(JSContext* cx, unsigned argc, Value* vp) { return true; } + // TaintFox + if(isTaintedNumber(args[0])){ + double x; + if (!ToNumber(cx, args[0], &x)) { + return false; + } + + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, RoundFloat32(x), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + return RoundFloat32(cx, args[0], args.rval()); } @@ -325,14 +393,37 @@ double js::math_max_impl(double x, double y) { bool js::math_max(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); + NumberObject *taintedResult = NumberObject::create(cx, 0); + bool isTainted = false; + double maxval = NegativeInfinity(); for (unsigned i = 0; i < args.length(); i++) { double x; if (!ToNumber(cx, args[i], &x)) { return false; } + maxval = math_max_impl(x, maxval); + + // TaintFox + if(isTaintedNumber(args[i]) && maxval == x){ + isTainted = true; + NumberObject *obj = &args[i].toObject().as(); + taintedResult->setTaint(obj->getTaintFlow()); + } + else if(maxval == x){ + //This is done since we only want to propogate the taint if it is the biggest value + // if a non tainted value is the biggest, we do not propogate the taint + isTainted = false; + } } + + if(isTainted){ + taintedResult->setPrimitiveValue(JS::NumberValue(maxval)); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(maxval); return true; } @@ -350,14 +441,37 @@ double js::math_min_impl(double x, double y) { bool js::math_min(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); + NumberObject *taintedResult = NumberObject::create(cx, 0); + bool isTainted = false; + double minval = PositiveInfinity(); for (unsigned i = 0; i < args.length(); i++) { double x; if (!ToNumber(cx, args[i], &x)) { return false; } + minval = math_min_impl(x, minval); + + // TaintFox + if(isTaintedNumber(args[i]) && minval == x){ + isTainted = true; + NumberObject *obj = &args[i].toObject().as(); + taintedResult->setTaint(obj->getTaintFlow()); + } + else if(minval == x){ + //This is done since we only want to propogate the taint if it is the lowest value + // if a non tainted value is the lowest, we do not propogate the taint + isTainted = false; + } } + + if(isTainted){ + taintedResult->setPrimitiveValue(JS::NumberValue(minval)); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(minval); return true; } @@ -479,6 +593,24 @@ static bool math_pow(JSContext* cx, unsigned argc, Value* vp) { } double z = ecmaPow(x, y); + + // TaintFox + NumberObject *taintedResult = NumberObject::create(cx, 0); + bool isTainted = false; + + for (unsigned i = 0; i < args.length(); i++) { + if(isTaintedNumber(args[i])){ + isTainted = true; + NumberObject *obj = &args[i].toObject().as(); + taintedResult->setTaint(TaintFlow::append(taintedResult->getTaintFlow(), obj->getTaintFlow())); + } + } + if(isTainted){ + taintedResult->setPrimitiveValue(JS::NumberValue(z)); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(z); return true; } @@ -589,6 +721,14 @@ static bool math_round(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, math_round_impl(x), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(math_round_impl(x)); return true; } @@ -791,6 +931,10 @@ static bool math_hypot(JSContext* cx, unsigned argc, Value* vp) { bool js::math_hypot_handle(JSContext* cx, HandleValueArray args, MutableHandleValue res) { + + NumberObject *taintedResult = NumberObject::create(cx, 0); + bool isTainted = false; + // IonMonkey calls the ecmaHypot function directly if two arguments are // given. Do that here as well to get the same results. if (args.length() == 2) { @@ -803,6 +947,21 @@ bool js::math_hypot_handle(JSContext* cx, HandleValueArray args, } double result = ecmaHypot(x, y); + + // TaintFox + for (unsigned i = 0; i < args.length(); i++) { + if(isTaintedNumber(args[i])){ + isTainted = true; + NumberObject *obj = &args[i].toObject().as(); + taintedResult->setTaint(TaintFlow::append(taintedResult->getTaintFlow(), obj->getTaintFlow())); + } + } + if(isTainted){ + taintedResult->setPrimitiveValue(JS::NumberValue(result)); + res.setObjectOrNull(taintedResult); + return true; + } + res.setDouble(result); return true; } @@ -825,12 +984,26 @@ bool js::math_hypot_handle(JSContext* cx, HandleValueArray args, continue; } + // TaintFox + if(isTaintedNumber(args[i])){ + isTainted = true; + NumberObject *obj = &args[i].toObject().as(); + taintedResult->setTaint(TaintFlow::append(taintedResult->getTaintFlow(), obj->getTaintFlow())); + } + hypot_step(scale, sumsq, x); } double result = isInfinite ? PositiveInfinity() : isNaN ? GenericNaN() : scale * std::sqrt(sumsq); + + if(isTainted){ + taintedResult->setPrimitiveValue(JS::NumberValue(result)); + res.setObjectOrNull(taintedResult); + return true; + } + res.setDouble(result); return true; } @@ -857,6 +1030,14 @@ bool js::math_trunc(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, math_trunc_impl(x), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(math_trunc_impl(x)); return true; } @@ -883,6 +1064,14 @@ static bool math_sign(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox + if(isTaintedNumber(args[0])){ + NumberObject *obj = &args[0].toObject().as(); + NumberObject *taintedResult = NumberObject::createTainted(cx, math_sign_impl(x), obj->getTaintFlow()); + args.rval().setObjectOrNull(taintedResult); + return true; + } + args.rval().setNumber(math_sign_impl(x)); return true; } diff --git a/js/src/jsnum.cpp b/js/src/jsnum.cpp index 979a6ab22cb09..8a026d73ec779 100644 --- a/js/src/jsnum.cpp +++ b/js/src/jsnum.cpp @@ -30,6 +30,7 @@ #include // memmove #include +#include "jstaint.h" #include "jstypes.h" #include "double-conversion/double-conversion.h" @@ -449,7 +450,12 @@ static bool num_parseFloat(JSContext* cx, unsigned argc, Value* vp) { return false; } - if (str->hasIndexValue()) { + // TaintFox: create copy of string taint to be safe in case string is + // affected by garbage collection + SafeStringTaint taint = str->taint().safeCopy(); + + // TaintFox: Only allow shortcut via index value for untainted strings + if (str->hasIndexValue() && !taint.hasTaint()) { args.rval().setNumber(str->getIndexValue()); return true; } @@ -460,6 +466,9 @@ static bool num_parseFloat(JSContext* cx, unsigned argc, Value* vp) { } double d; + // TaintFox: add scope around AutoCheckCannotGC because it would later + // interfer with creating the NumberObject + { AutoCheckCannotGC nogc; if (linear->hasLatin1Chars()) { const Latin1Char* begin = linear->latin1Chars(nogc); @@ -476,8 +485,17 @@ static bool num_parseFloat(JSContext* cx, unsigned argc, Value* vp) { d = GenericNaN(); } } + } - args.rval().setDouble(d); + // TaintFox: if string was tainted, propagate taint to number using NumberObj + if (taint.hasTaint()) { + // Currently gets taint from any one TaintRange in tainted string. + // In the future, it should get all taints. + args.rval().setObject(*NumberObject::createTainted(cx, d, taint.begin()->flow())); + } else { + // Default case from original code: set number primitive + args.rval().setDouble(d); + } return true; } @@ -549,6 +567,7 @@ bool js::NumberParseInt(JSContext* cx, HandleString str, int32_t radix, MOZ_ASSERT(2 <= radix && radix <= 36); JSLinearString* linear = str->ensureLinear(cx); + const StringTaint& taint = linear->taint(); if (!linear) { return false; } @@ -569,7 +588,16 @@ bool js::NumberParseInt(JSContext* cx, HandleString str, int32_t radix, } } - result.setNumber(number); + // TaintFox: if string was tainted, propagate taint to number using NumberObj + if (taint.hasTaint()) { + // Currently gets taint from any one TaintRange in tainted string. + // In the future, it should get all taints. + result.setObject(*NumberObject::createTainted(cx, number, taint.begin()->flow())); + } else { + // Default case from original code: set number primitive + result.setNumber(number); + } + return true; } @@ -622,7 +650,8 @@ static bool num_parseInt(JSContext* cx, unsigned argc, Value* vp) { if (args[0].isString()) { JSString* str = args[0].toString(); - if (str->hasIndexValue()) { + // TaintFox: Only allow shortcut via index value for untainted strings + if (str->hasIndexValue() && str->taint().hasTaint()) { args.rval().setNumber(str->getIndexValue()); return true; } @@ -635,6 +664,10 @@ static bool num_parseInt(JSContext* cx, unsigned argc, Value* vp) { return false; } + // TaintFox: create copy of string taint to be safe in case string is + // affected by garbage collection + SafeStringTaint taint = inputString->taint().safeCopy(); + // Step 6. int32_t radix = 0; if (args.hasDefined(1)) { @@ -654,12 +687,19 @@ static const JSFunctionSpec number_functions[] = { const JSClass NumberObject::class_ = { "Number", - JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_HAS_CACHED_PROTO(JSProto_Number), + JSCLASS_HAS_RESERVED_SLOTS(2) | JSCLASS_HAS_CACHED_PROTO(JSProto_Number), JS_NULL_CLASS_OPS, &NumberObject::classSpec_}; static bool Number(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); + // TaintFox: create copy of string taint to be safe in case string is + // affected by garbage collection + SafeStringTaint taint; + if (args[0].isString()){ + taint = args[0].toString()->taint().safeCopy(); + } + if (args.length() > 0) { // BigInt proposal section 6.2, steps 2a-c. if (!ToNumeric(cx, args[0])) { @@ -673,7 +713,16 @@ static bool Number(JSContext* cx, unsigned argc, Value* vp) { if (!args.isConstructing()) { if (args.length() > 0) { - args.rval().set(args[0]); + + // Taintfox: this takes care of [Number()] without the explicit new. By + // default it will output a number, not a NumberObject. If a tainted + // string was used as an argument, we create a tainted NumberObject. + if (taint.hasTaint()) { + args.rval().setObject(*NumberObject::createTainted(cx, args[0].toNumber(), taint.begin()->flow())); + } else{ + // Default branch from original code + args.rval().set(args[0]); + } } else { args.rval().setInt32(0); } @@ -686,7 +735,17 @@ static bool Number(JSContext* cx, unsigned argc, Value* vp) { } double d = args.length() > 0 ? args[0].toNumber() : 0; - JSObject* obj = NumberObject::create(cx, d, proto); + + // Taintfox: this takes care of explicit [new Number()] if a string was used + // as an argument + JSObject* obj; + if (taint.hasTaint()) { + obj = NumberObject::createTainted(cx, d, taint.begin()->flow()); + } else{ + // Default branch from original code + obj = NumberObject::create(cx, d, proto); + } + if (!obj) { return false; } @@ -741,11 +800,18 @@ static bool num_toSource(JSContext* cx, unsigned argc, Value* vp) { } JSStringBuilder sb(cx); - if (!sb.append("(new Number(") || - !NumberValueToStringBuffer(NumberValue(d), sb) || !sb.append("))")) { - return false; + // TaintFox: Hide the fact that tainted numbers are NumberObjects. + if (isTaintedNumber(args.thisv())) { + if (!NumberValueToStringBuffer(NumberValue(d), sb)) { + return false; + } + } else { + if (!sb.append("(new Number(") || + !NumberValueToStringBuffer(NumberValue(d), sb) || !sb.append("))")) { + return false; + } } - + JSString* str = sb.finishString(); if (!str) { return false; @@ -1014,6 +1080,19 @@ static bool num_toString(JSContext* cx, unsigned argc, Value* vp) { if (!str) { return false; } + + // TaintFox: Propagate Number Taint to String + if (isTaintedNumber(args.thisv())) { + // Atoms cannot be tainted. Atoms are created for ints<6bit and other + // common strings. If we are dealing with an Atom, wrap it inside a + // dependent String, which can be tainted. + if(str->isAtom()){ + str = NewDependentString(cx,str,0,str->length()); + } + SafeStringTaint newTaint(getNumberTaint(args.thisv()),str->length()); + str->setTaint(cx, newTaint); + } + args.rval().setString(str); return true; } @@ -1428,6 +1507,63 @@ static const JSFunctionSpec number_methods[] = { JS_FN("toPrecision", num_toPrecision, 1, 0), JS_FS_END}; +static bool num_taint_getter(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setNull(); + + // This will be the case for unboxed integers. In that case just return null. + if (!args.thisv().isObject()) { + return true; + } + + RootedObject number(cx, &args.thisv().toObject()); + if (!number->is()) { + return true; + } + + // Return, if number is not tainted + if (!number->as().isTainted()) { + return true; + } + + const TaintFlow& taint = number->as().taint(); + + RootedObject taint_obj(cx, JS_NewObject(cx, nullptr)); + if (!getTaintFlowObject(cx, taint, taint_obj)) { + return false; + } + + args.rval().setObject(*taint_obj); + return true; +} + +/* TaintFox: Add |taint| property. */ +static const +JSPropertySpec number_taint_properties[] = { + JS_PSG("taint", num_taint_getter, JSPROP_PERMANENT), + JS_PS_END +}; + +// TaintFox: taint numbers manually using this method. +bool +js::Number_tainted(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + double d; + if (!ToNumber(cx, args.get(0), &d)) { + return false; + } + TaintOperation op("manual taint source", { taintarg(cx, d) }); + op.setSource(); + JSObject* number = NumberObject::createTainted(cx, d, TaintFlow(op)); + + args.rval().setObject(*number); + + return true; +} + bool js::IsInteger(double d) { return std::isfinite(d) && JS::ToInteger(d) == d; } @@ -1437,6 +1573,7 @@ static const JSFunctionSpec number_static_methods[] = { JS_SELF_HOSTED_FN("isInteger", "Number_isInteger", 1, 0), JS_SELF_HOSTED_FN("isNaN", "Number_isNaN", 1, 0), JS_SELF_HOSTED_FN("isSafeInteger", "Number_isSafeInteger", 1, 0), + JS_FN("tainted", Number_tainted, 1,0), JS_FS_END}; static const JSPropertySpec number_static_properties[] = { @@ -1501,7 +1638,7 @@ bool js::InitRuntimeNumberState(JSRuntime* rt) { if (!storage) { return false; } - + js_memcpy(storage, thousandsSeparator, thousandsSeparatorSize); rt->thousandsSeparator = storage; storage += thousandsSeparatorSize; @@ -1601,7 +1738,7 @@ const ClassSpec NumberObject::classSpec_ = { number_static_methods, number_static_properties, number_methods, - nullptr, + number_taint_properties, NumberClassFinish}; static char* FracNumberToCString(ToCStringBuf* cbuf, double d, size_t* len) { @@ -2063,7 +2200,7 @@ bool js::ToNumericSlow(JSContext* cx, MutableHandleValue vp) { MOZ_ASSERT(!vp.isNumeric()); // Step 1. - if (!vp.isPrimitive()) { + if (!vp.isPrimitive() && !isTaintedNumber(vp)) { if (!ToPrimitive(cx, JSTYPE_NUMBER, vp)) { return false; } @@ -2197,7 +2334,7 @@ bool js::ToInt32OrBigIntSlow(JSContext* cx, MutableHandleValue vp) { return true; } - if (!ToNumeric(cx, vp)) { + if (!ToNumericUnboxTainted(cx, vp)) { return false; } diff --git a/js/src/jsnum.h b/js/src/jsnum.h index 5e0cf439e3797..157350a1e5263 100644 --- a/js/src/jsnum.h +++ b/js/src/jsnum.h @@ -13,6 +13,8 @@ #include +#include "jstaint.h" + #include "NamespaceImports.h" #include "js/Conversions.h" @@ -28,6 +30,10 @@ class TaggedParserAtomIndex; } // namespace frontend class GlobalObject; + +// TaintFox: Exported for the js shell: taint(number). +bool Number_tainted(JSContext* cx, unsigned argc, Value* vp); + class StringBuffer; [[nodiscard]] extern bool InitRuntimeNumberState(JSRuntime* rt); @@ -232,9 +238,10 @@ extern bool NumberParseInt(JSContext* cx, JS::HandleString str, int32_t radix, JS::MutableHandleValue result); /* ES5 9.3 ToNumber, overwriting *vp with the appropriate number value. */ + [[nodiscard]] MOZ_ALWAYS_INLINE bool ToNumber(JSContext* cx, JS::MutableHandleValue vp) { - if (vp.isNumber()) { + if (vp.isNumber() || isTaintedNumber(vp)) { return true; } double d; @@ -259,6 +266,24 @@ bool ToNumericSlow(JSContext* cx, JS::MutableHandleValue vp); return ToNumericSlow(cx, vp); } +// Additional function to also convert tainted numbers to real numerics (to toNumber() works as expected) +[[nodiscard]] MOZ_ALWAYS_INLINE bool ToNumericUnboxTainted(JSContext* cx, + JS::MutableHandleValue vp) { + if (!ToNumeric(cx, vp)) { + return false; + } + + if (JS::isTaintedNumber(vp)) { + // Also unbox the tainted numbers + double d; + if (!ToNumber(cx, vp, &d)) + return false; + vp.setNumber(d); + } + + return true; +} + bool ToInt32OrBigIntSlow(JSContext* cx, JS::MutableHandleValue vp); [[nodiscard]] MOZ_ALWAYS_INLINE bool ToInt32OrBigInt( diff --git a/js/src/jstaint.cpp b/js/src/jstaint.cpp index 22a4da241f96a..d7a232d62f39e 100644 --- a/js/src/jstaint.cpp +++ b/js/src/jstaint.cpp @@ -11,14 +11,18 @@ #include #include + #include "jsapi.h" +#include "NamespaceImports.h" #include "js/Array.h" #include "js/CharacterEncoding.h" #include "js/ErrorReport.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions #include "js/UniquePtr.h" #include "vm/FrameIter.h" #include "vm/JSContext.h" #include "vm/JSFunction.h" +#include "vm/NumberObject.h" #include "vm/StringType.h" using namespace JS; @@ -362,15 +366,238 @@ void JS::MarkTaintedFunctionArguments(JSContext* cx, JSFunction* function, const for (unsigned i = 0; i < args.length(); i++) { if (args[i].isString()) { RootedString arg(cx, args[i].toString()); - if (arg->isTainted()) { - arg->taint().extend( - TaintOperation("function", location, - { taintarg(cx, name), sourceinfo, taintarg(cx, i), taintarg(cx, args.length()) } )); - } } } } +bool JS::isTaintedNumber(const Value& val) +{ + if (val.isObject() && val.toObject().is()) { + NumberObject& number = val.toObject().as(); + return number.isTainted(); + } + return false; +} + +bool JS::isTaintedValue(const Value& val) +{ + if (val.isObject() && val.toObject().is()) { + NumberObject& number = val.toObject().as(); + return number.isTainted(); + } else if (val.isString()) { + return val.toString()->isTainted(); + } + return false; +} + +const TaintFlow& JS::getValueTaint(const Value& val) +{ + if (val.isObject() && val.toObject().is()) { + NumberObject& number = val.toObject().as(); + return number.taint(); + } else if (val.isString()) { + for (auto& range: val.toString()->Taint()) { + // Just return first taint range + return range.flow(); + } + } + return TaintFlow::getEmptyTaintFlow(); +} + +const TaintFlow& JS::getNumberTaint(const Value& val) +{ + if (val.isObject() && val.toObject().is()) { + NumberObject& number = val.toObject().as(); + return number.taint(); + } + return TaintFlow::getEmptyTaintFlow(); +} + +bool JS::isAnyTaintedNumber(const Value& val1, const Value& val2) +{ + return isTaintedNumber(val1) || isTaintedNumber(val2); +} + +bool JS::isAnyTaintedValue(const Value& val1, const Value& val2) +{ + return isTaintedValue(val1) || isTaintedValue(val2); +} + +TaintFlow JS::getAnyNumberTaint(const Value& val1, const Value& val2, const char* name) +{ + // add info for operation + // add getting combined taint flow + if (isTaintedNumber(val1) && isTaintedNumber(val2) && (val1 != val2)) { + // Use a very simple taint operation here to keep things fast + return TaintFlow::append(getNumberTaint(val1), getNumberTaint(val2), TaintOperation(name)); + } else if (isTaintedNumber(val1)) { + return getNumberTaint(val1); + } else { + return getNumberTaint(val2); + } +} + +TaintFlow JS::getAnyValueTaint(const Value& val1, const Value& val2, const char* name) +{ + // add info for operation + // add getting combined taint flow + if (isTaintedValue(val1) && isTaintedValue(val2) && (val1 != val2)) { + // Use a very simple taint operation here to keep things fast + return TaintFlow::append(getValueTaint(val1), getValueTaint(val2), TaintOperation(name)); + } else if (isTaintedValue(val1)) { + return getValueTaint(val1); + } else { + return getValueTaint(val2); + } +} + +bool JS::getTaintOperationObject(JSContext* cx, const TaintOperation& op, JS::Handle node) +{ + if (!node) + return false; + + RootedString operation(cx, JS_NewStringCopyZ(cx, op.name())); + if (!operation) + return false; + + if (!JS_DefineProperty(cx, node, "operation", operation, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + RootedValue isBuiltIn(cx); + isBuiltIn.setBoolean(op.is_native()); + + if (!JS_DefineProperty(cx, node, "builtin", isBuiltIn, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + RootedValue isSource(cx); + isSource.setBoolean(op.isSource()); + + if (!JS_DefineProperty(cx, node, "source", isSource, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + // Wrap the location + RootedObject location(cx, JS_NewObject(cx, nullptr)); + if (!location) + return false; + RootedString filename(cx, JS_NewUCStringCopyZ(cx, op.location().filename().c_str())); + if (!filename) + return false; + RootedString function(cx, JS_NewUCStringCopyZ(cx, op.location().function().c_str())); + if (!function) + return false; + // Also add the MD5 hash of the containing function + RootedString hash(cx, JS_NewStringCopyZ(cx, JS::convertDigestToHexString(op.location().scriptHash()).c_str())); + if (!hash) + return false; + + if (!JS_DefineProperty(cx, location, "filename", filename, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || + !JS_DefineProperty(cx, location, "function", function, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || + !JS_DefineProperty(cx, location, "line", op.location().line(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || + !JS_DefineProperty(cx, location, "pos", op.location().pos(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || + !JS_DefineProperty(cx, location, "scriptline", op.location().scriptStartLine(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || + !JS_DefineProperty(cx, location, "scripthash", hash, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + if (!JS_DefineProperty(cx, node, "location", location, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + // Wrap the arguments + RootedValueVector taint_arguments(cx); + for (auto& taint_argument : op.arguments()) { + RootedString argument(cx, JS_NewUCStringCopyZ(cx, taint_argument.c_str())); + if (!argument) + return false; + + if (!taint_arguments.append(StringValue(argument))) + return false; + } + + RootedObject arguments(cx, NewDenseCopiedArray(cx, taint_arguments.length(), taint_arguments.begin())); + if (!JS_DefineProperty(cx, node, "arguments", arguments, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + return true; +} + +bool JS::getTaintFlowObject(JSContext* cx, const TaintFlow& flow, JS::Handle obj) +{ + if(!obj) + return false; + + // Wrap the taint flow for the current range. + RootedValueVector taint_flow(cx); + for (TaintNodeBase& taint_node : flow) { + RootedObject node(cx, JS_NewObject(cx, nullptr)); + if (!node) + return false; + if (!getTaintOperationObject(cx, taint_node.operation(), node)) { + return false; + } + if (!taint_flow.append(ObjectValue(*node))) { + return false; + } + } + + RootedObject flow_obj(cx, NewDenseCopiedArray(cx, taint_flow.length(), taint_flow.begin())); + if (!flow_obj) + return false; + if (!JS_DefineProperty(cx, obj, "flow", flow_obj, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + // Also output the sources + RootedValueVector sources(cx); + for (TaintOperation op: flow.getSources()) { + RootedObject node(cx, JS_NewObject(cx, nullptr)); + if (!node) + return false; + if (!getTaintOperationObject(cx, op, node)) { + return false; + } + if (!sources.append(ObjectValue(*node))) { + return false; + } + } + + RootedObject sources_obj(cx, NewDenseCopiedArray(cx, sources.length(), sources.begin())); + if (!sources_obj) { + return false; + } + if (!JS_DefineProperty(cx, obj, "sources", sources_obj, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + return true; +} + +bool JS::getStringTaintObject(JSContext* cx, const StringTaint& taint, JS::Handle result) +{ + // Wrap all taint ranges of the string. + RootedValueVector ranges(cx); + for (const TaintRange& taint_range : taint) { + RootedObject range(cx, JS_NewObject(cx, nullptr)); + if(!range) + return false; + + if (!JS_DefineProperty(cx, range, "begin", taint_range.begin(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT) || + !JS_DefineProperty(cx, range, "end", taint_range.end(), JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + if (!getTaintFlowObject(cx, taint_range.flow(), range)) { + return false; + } + + if (!ranges.append(ObjectValue(*range))) + return false; + } + + RootedObject ranges_obj(cx, NewDenseCopiedArray(cx, ranges.length(), ranges.begin())); + if (!ranges_obj) + return false; + if (!JS_DefineProperty(cx, result, "ranges", ranges_obj, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) + return false; + + return true; +} + // Print a message to stdout. void JS::TaintFoxReport(JSContext* cx, const char* msg) { diff --git a/js/src/jstaint.h b/js/src/jstaint.h index 767f48c302850..e2d71a9cfbd0a 100644 --- a/js/src/jstaint.h +++ b/js/src/jstaint.h @@ -85,6 +85,39 @@ TaintOperation TaintOperationFromContext(JSContext* cx, const char* name, bool i // This is mainly useful for tracing tainted arguments through the code. void MarkTaintedFunctionArguments(JSContext* cx, JSFunction* function, const JS::CallArgs& args); +// Check if the argument value is a tainted number object. +bool isTaintedNumber(const JS::Value& val); + +// Check if the argument value is a tainted number object. +bool isTaintedValue(const JS::Value& val); + +// Extract the taint information from a number. +const TaintFlow& getNumberTaint(const JS::Value& val); + +// Extract the taint information from a number. +const TaintFlow& getValueTaint(const JS::Value& val); + +// Check if any of the argument values is a tainted number object. +// TODO make this accept a variable amount of arguments using variadic templates +bool isAnyTaintedNumber(const JS::Value& val1, const JS::Value& val2); + +// Check if any of the argument values is a tainted number object. +// TODO make this accept a variable amount of arguments using variadic templates +bool isAnyTaintedValue(const JS::Value& val1, const JS::Value& val2); + +// Extract the taint information from the first tainted number argument. +// TODO make this accept a variable amount of arguments using variadic templates +TaintFlow getAnyNumberTaint(const JS::Value& val1, const JS::Value& val2, const char* name); + +// Extract the taint information from the first tainted argument. +TaintFlow getAnyValueTaint(const JS::Value& val1, const JS::Value& val2, const char* name); + +bool getTaintOperationObject(JSContext* cx, const TaintOperation& op, JS::Handle result); + +bool getTaintFlowObject(JSContext* cx, const TaintFlow& flow, JS::Handle result); + +bool getStringTaintObject(JSContext* cx, const StringTaint& taint, JS::Handle result); + // Print a message to stdout. void TaintFoxReport(JSContext* cx, const char* msg); diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index caefdbe2298dc..a8d1ebcdab47e 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -8572,7 +8572,19 @@ static bool EntryPoints(JSContext* cx, unsigned argc, Value* vp) { static bool Taint(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (args[0].isNumber()) { + return Number_tainted(cx, argc, vp); + } else { return str_tainted(cx, argc, vp); + } } #ifndef __wasi__ diff --git a/js/src/tests/non262/taint/arrays.js b/js/src/tests/non262/taint/arrays.js new file mode 100644 index 0000000000000..8d9903cd81d42 --- /dev/null +++ b/js/src/tests/non262/taint/arrays.js @@ -0,0 +1,41 @@ +function indexOfTestTaintedValue() { + let tnum = taint(42); + let nums_t = [1,tnum,3]; + let nums_ut = [1,42,3]; + let idx_t = nums_t.indexOf(42); + let idx_ut = nums_ut.indexOf(42); + assertEq(idx_ut, idx_t); +} + +function indexOfTestTaintedKey() { + let tnum = taint(42); + let nums = [1,42,3]; + let idx_t = nums.indexOf(tnum); + let idx_ut = nums.indexOf(42); + assertEq(idx_ut, idx_t); +} + +function includesTestTaintedKey() { + let tnum = taint(42); + let nums = [1,42,3]; + let inc_t = nums.includes(tnum); + let inc_ut = nums.includes(42); + assertEq(inc_ut, inc_t); +} + +function includesTestTaintedValue() { + let tnum = taint(42); + let nums = [1,tnum,3]; + let inc_t = nums.includes(tnum); + let inc_ut = nums.includes(42); + assertEq(inc_ut, inc_t); +} + +runTaintTest(indexOfTestTaintedValue); +runTaintTest(indexOfTestTaintedKey); +runTaintTest(includesTestTaintedKey); +runTaintTest(includesTestTaintedValue); + + +if (typeof reportCompare === 'function') + reportCompare(true, true); diff --git a/js/src/tests/non262/taint/base64.js b/js/src/tests/non262/taint/base64.js new file mode 100644 index 0000000000000..dc7daf3c6270a --- /dev/null +++ b/js/src/tests/non262/taint/base64.js @@ -0,0 +1,89 @@ +// Base64 code taken from https://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/Base64 + +var base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(""); +var base64inv = {}; +for (var i = 0; i < base64chars.length; i++) { + base64inv[base64chars[i]] = i; +} + +function base64_encode(s) +{ + // the result/encoded string, the padding string, and the pad count + var r = ""; + var p = ""; + var c = s.length % 3; + + // add a right zero pad to make this string a multiple of 3 characters + if (c > 0) { + for (; c < 3; c++) { + p += '='; + s += "\0"; + } + } + + // increment over the length of the string, three characters at a time + for (c = 0; c < s.length; c += 3) { + + // we add newlines after every 76 output characters, according to the MIME specs + if (c > 0 && (c / 3 * 4) % 76 == 0) { + r += "\r\n"; + } + + // these three 8-bit (ASCII) characters become one 24-bit number + var n = (s.charCodeAt(c) << 16) + (s.charCodeAt(c+1) << 8) + s.charCodeAt(c+2); + + // this 24-bit number gets separated into four 6-bit numbers + n = [(n >>> 18) & 63, (n >>> 12) & 63, (n >>> 6) & 63, n & 63]; + + // those four 6-bit numbers are used as indices into the base64 character list + r += base64chars[n[0]] + base64chars[n[1]] + base64chars[n[2]] + base64chars[n[3]]; + } + // add the actual padding string, after removing the zero pad + return r.substring(0, r.length - p.length) + p; +} + +function base64_decode(s) +{ + // remove/ignore any characters not in the base64 characters list + // or the pad character -- particularly newlines + s = s.replace(new RegExp('[^'+base64chars.join("")+'=]', 'g'), ""); + + // replace any incoming padding with a zero pad (the 'A' character is zero) + var p = (s.charAt(s.length-1) == '=' ? + (s.charAt(s.length-2) == '=' ? 'AA' : 'A') : ""); + var r = ""; + s = s.substr(0, s.length - p.length) + p; + + // increment over the length of this encoded string, four characters at a time + for (var c = 0; c < s.length; c += 4) { + + // each of these four characters represents a 6-bit index in the base64 characters list + // which, when concatenated, will give the 24-bit number for the original 3 characters + var n = (base64inv[s.charAt(c)] << 18) + (base64inv[s.charAt(c+1)] << 12) + + (base64inv[s.charAt(c+2)] << 6) + base64inv[s.charAt(c+3)]; + + // split the 24-bit number into the original three 8-bit (ASCII) characters + r += String.fromCharCode((n >>> 16) & 255, (n >>> 8) & 255, n & 255); + } + // remove any zero pad that was added to make this a multiple of 24 bits + return r.substring(0, r.length - p.length); +} + +function base64TaintTest() { + //var s = randomMultiTaintedString(); + var s = randomTaintedString(); + + assertTainted(base64_encode(s)); + print(s); + print(stringifyTaint(s.taint)); + print(base64_encode(s)); + print(base64_encode(s).length); + print(stringifyTaint(base64_encode(s).taint)); + + assertEqualTaint(s, base64_decode(base64_encode(s))); +} + +runTaintTest(base64TaintTest); + +if (typeof reportCompare === 'function') + reportCompare(true, true); diff --git a/js/src/tests/non262/taint/element_access.js b/js/src/tests/non262/taint/element_access.js new file mode 100644 index 0000000000000..b5f6045b0172d --- /dev/null +++ b/js/src/tests/non262/taint/element_access.js @@ -0,0 +1,20 @@ +function elementAccesstest() { + var str = randomString(); + var index = rand(0, str.length); + var taintedIndex = taint(index); + + // Short strings/characters accessd through a tainted index should also be tainted + // as they could represent lookup tables. + + assertTainted(str[taintedIndex]); + assertNotTainted(str[index]); + + var chars = str.split(''); + assertTainted(chars[taintedIndex]); + assertNotTainted(chars[index]); +} + +runTaintTest(elementAccesstest); + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/non262/taint/hash.js b/js/src/tests/non262/taint/hash.js new file mode 100644 index 0000000000000..be5330cac742c --- /dev/null +++ b/js/src/tests/non262/taint/hash.js @@ -0,0 +1,437 @@ +/* + This test contains a variety of hashing functions. + Both very simple ones as well as more complicated and obfoscated ones. + Some are based on the hashing functions used by fingerprinting scripts. +*/ + +noHash = (inputString)=>{ + var hash = 0; + if (inputString.length == 0) return hash; + for (i = 0; i < inputString.length; i++) { + var c = inputString.charCodeAt(i); + hash = hash + c; + hash = hash & hash; // Convert to 32bit integer + } + return hash; +} + + +// Based on: https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ +simpleHash = (inputString)=>{ + var hash = 0; + if (inputString.length == 0) return hash; + for (i = 0; i < inputString.length; i++) { + var c = inputString.charCodeAt(i); + hash = ((hash << 5) - hash) + c; + hash = hash & hash; // Convert to 32bit integer + } + return hash; +} + +// Based on: http://www.myersdaily.org/joseph/javascript/md5.js +// More details at: http://www.myersdaily.org/joseph/javascript/md5-text.html +md5Hash = (inputString)=>{ + md5cycle = (x, k) => { + var a = x[0], b = x[1], c = x[2], d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + + } + + cmn = (q, a, b, x, s, t) => { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + } + + ff = (a, b, c, d, x, s, t) => { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + } + + gg = (a, b, c, d, x, s, t) => { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + } + + hh = (a, b, c, d, x, s, t) => { + return cmn(b ^ c ^ d, a, b, x, s, t); + } + + ii = (a, b, c, d, x, s, t) => { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + } + + md51 = (s) => { + txt = ''; + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], i; + for (i=64; i<=s.length; i+=64) { + md5cycle(state, md5blk(s.substring(i-64, i))); + } + s = s.substring(i-64); + var tail = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]; + for (i=0; i>2] |= s.charCodeAt(i) << ((i%4) << 3); + tail[i>>2] |= 0x80 << ((i%4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i=0; i<16; i++) tail[i] = 0; + } + tail[14] = n*8; + md5cycle(state, tail); + return state; + } + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = (s) => { /* I figured global was faster. */ + var md5blks = [], i; /* Andy King said do it this way. */ + for (i=0; i<64; i+=4) { + md5blks[i>>2] = s.charCodeAt(i) + + (s.charCodeAt(i+1) << 8) + + (s.charCodeAt(i+2) << 16) + + (s.charCodeAt(i+3) << 24); + } + return md5blks; + } + + var hex_chr = '0123456789abcdef'.split(''); + + rhex = (n) => + { + var s='', j=0; + for(; j<4; j++) + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + + hex_chr[(n >> (j * 8)) & 0x0F]; + return s; + } + + hex = (x) => { + for (var i=0; i { + return hex(md51(s)); + } + + /* this is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + + add32 = (a, b) => { + return (a + b) & 0xFFFFFFFF; + } + + if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') { + add32 = (x, y) => { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } + } + + return md5(inputString); +} + +// Based on the hashing algorithm used in FingerprintJS v3.3.3 by Fingerprint Inc, +// which itself is based on MurmurHash3 by Karan Lyons +// FingerprintJS: https://github.com/fingerprintjs/fingerprintjs +// MurmurHash3: https://github.com/karanlyons/murmurHash3.js +// Minified code +fingerprintJsHash = (inputString)=>{ + c = (e, t) => { + e = [e[0] >>> 16, 65535 & e[0], e[1] >>> 16, 65535 & e[1]], + t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]]; + var n = [0, 0, 0, 0]; + return n[3] += e[3] + t[3], + n[2] += n[3] >>> 16, + n[3] &= 65535, + n[2] += e[2] + t[2], + n[1] += n[2] >>> 16, + n[2] &= 65535, + n[1] += e[1] + t[1], + n[0] += n[1] >>> 16, + n[1] &= 65535, + n[0] += e[0] + t[0], + n[0] &= 65535, + [n[0] << 16 | n[1], n[2] << 16 | n[3]] + } + u = (e, t) => { + e = [e[0] >>> 16, 65535 & e[0], e[1] >>> 16, 65535 & e[1]], + t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]]; + var n = [0, 0, 0, 0]; + return n[3] += e[3] * t[3], + n[2] += n[3] >>> 16, + n[3] &= 65535, + n[2] += e[2] * t[3], + n[1] += n[2] >>> 16, + n[2] &= 65535, + n[2] += e[3] * t[2], + n[1] += n[2] >>> 16, + n[2] &= 65535, + n[1] += e[1] * t[3], + n[0] += n[1] >>> 16, + n[1] &= 65535, + n[1] += e[2] * t[2], + n[0] += n[1] >>> 16, + n[1] &= 65535, + n[1] += e[3] * t[1], + n[0] += n[1] >>> 16, + n[1] &= 65535, + n[0] += e[0] * t[3] + e[1] * t[2] + e[2] * t[1] + e[3] * t[0], + n[0] &= 65535, + [n[0] << 16 | n[1], n[2] << 16 | n[3]] + } + s = (e, t) => { + return 32 === (t %= 64) ? [e[1], e[0]] : t < 32 ? [e[0] << t | e[1] >>> 32 - t, e[1] << t | e[0] >>> 32 - t] : (t -= 32, + [e[1] << t | e[0] >>> 32 - t, e[0] << t | e[1] >>> 32 - t]) + } + l = (e, t) => { + return 0 === (t %= 64) ? e : t < 32 ? [e[0] << t | e[1] >>> 32 - t, e[1] << t] : [e[1] << t - 32, 0] + } + d = (e, t) => { + return [e[0] ^ t[0], e[1] ^ t[1]] + } + f = (e) => { + return e = d(e, [0, e[0] >>> 1]), + e = d(e = u(e, [4283543511, 3981806797]), [0, e[0] >>> 1]), + e = d(e = u(e, [3301882366, 444984403]), [0, e[0] >>> 1]) + } + h = (e, t) => { + t = t || 0; + var n, r = (e = e || "").length % 16, a = e.length - r, o = [0, t], i = [0, t], h = [0, 0], v = [0, 0], p = [2277735313, 289559509], m = [1291169091, 658871167]; + for (n = 0; n < a; n += 16) + h = [255 & e.charCodeAt(n + 4) | (255 & e.charCodeAt(n + 5)) << 8 | (255 & e.charCodeAt(n + 6)) << 16 | (255 & e.charCodeAt(n + 7)) << 24, 255 & e.charCodeAt(n) | (255 & e.charCodeAt(n + 1)) << 8 | (255 & e.charCodeAt(n + 2)) << 16 | (255 & e.charCodeAt(n + 3)) << 24], + v = [255 & e.charCodeAt(n + 12) | (255 & e.charCodeAt(n + 13)) << 8 | (255 & e.charCodeAt(n + 14)) << 16 | (255 & e.charCodeAt(n + 15)) << 24, 255 & e.charCodeAt(n + 8) | (255 & e.charCodeAt(n + 9)) << 8 | (255 & e.charCodeAt(n + 10)) << 16 | (255 & e.charCodeAt(n + 11)) << 24], + h = s(h = u(h, p), 31), + o = c(o = s(o = d(o, h = u(h, m)), 27), i), + o = c(u(o, [0, 5]), [0, 1390208809]), + v = s(v = u(v, m), 33), + i = c(i = s(i = d(i, v = u(v, p)), 31), o), + i = c(u(i, [0, 5]), [0, 944331445]); + switch (h = [0, 0], + v = [0, 0], + r) { + case 15: + v = d(v, l([0, e.charCodeAt(n + 14)], 48)); + case 14: + v = d(v, l([0, e.charCodeAt(n + 13)], 40)); + case 13: + v = d(v, l([0, e.charCodeAt(n + 12)], 32)); + case 12: + v = d(v, l([0, e.charCodeAt(n + 11)], 24)); + case 11: + v = d(v, l([0, e.charCodeAt(n + 10)], 16)); + case 10: + v = d(v, l([0, e.charCodeAt(n + 9)], 8)); + case 9: + v = u(v = d(v, [0, e.charCodeAt(n + 8)]), m), + i = d(i, v = u(v = s(v, 33), p)); + case 8: + h = d(h, l([0, e.charCodeAt(n + 7)], 56)); + case 7: + h = d(h, l([0, e.charCodeAt(n + 6)], 48)); + case 6: + h = d(h, l([0, e.charCodeAt(n + 5)], 40)); + case 5: + h = d(h, l([0, e.charCodeAt(n + 4)], 32)); + case 4: + h = d(h, l([0, e.charCodeAt(n + 3)], 24)); + case 3: + h = d(h, l([0, e.charCodeAt(n + 2)], 16)); + case 2: + h = d(h, l([0, e.charCodeAt(n + 1)], 8)); + case 1: + h = u(h = d(h, [0, e.charCodeAt(n)]), p), + o = d(o, h = u(h = s(h, 31), m)) + } + return o = c(o = d(o, [0, e.length]), i = d(i, [0, e.length])), + i = c(i, o), + o = c(o = f(o), i = f(i)), + i = c(i, o), + ("00000000" + (o[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (o[1] >>> 0).toString(16)).slice(-8) + ("00000000" + (i[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (i[1] >>> 0).toString(16)).slice(-8) + } + + return h(inputString); +} + +// Based on the hashing algorithm used in fingerprinting script found by +// FpFlow (https://github.com/FPFlow/FPFlow-project) +// Originally at: https://wl.jd.com/wl.js +// Stored at FpFlow: https://github.com/FPFlow/FPFlow-project/blob/master/scripts/4.formatted.js +// Minified code +fpFlow4Hash = (inputString)=>{ + + MD5 = { + chrsz: 8, + G: "", + hex_md5: function(e) { + return this.binl2hex(this.core_md5(this.str2binl(e), e.length * this.chrsz)) + }, + core_md5: function(e, t) { + e[t >> 5] |= 128 << t % 32, e[14 + (t + 64 >>> 9 << 4)] = t; + for (var r = 1732584193, n = -271733879, i = -1732584194, o = 271733878, a = 0; a < e.length; a += 16) { + var d = r, + s = n, + c = i, + u = o; + r = this.md5_ff(r, n, i, o, e[a + 0], 7, -680876936), o = this.md5_ff(o, r, n, i, e[a + 1], 12, -389564586), i = this.md5_ff(i, o, r, n, e[a + 2], 17, 606105819), n = this.md5_ff(n, i, o, r, e[a + 3], 22, -1044525330), r = this.md5_ff(r, n, i, o, e[a + 4], 7, -176418897), o = this.md5_ff(o, r, n, i, e[a + 5], 12, 1200080426), i = this.md5_ff(i, o, r, n, e[a + 6], 17, -1473231341), n = this.md5_ff(n, i, o, r, e[a + 7], 22, -45705983), r = this.md5_ff(r, n, i, o, e[a + 8], 7, 1770035416), o = this.md5_ff(o, r, n, i, e[a + 9], 12, -1958414417), i = this.md5_ff(i, o, r, n, e[a + 10], 17, -42063), n = this.md5_ff(n, i, o, r, e[a + 11], 22, -1990404162), r = this.md5_ff(r, n, i, o, e[a + 12], 7, 1804603682), o = this.md5_ff(o, r, n, i, e[a + 13], 12, -40341101), i = this.md5_ff(i, o, r, n, e[a + 14], 17, -1502002290), n = this.md5_ff(n, i, o, r, e[a + 15], 22, 1236535329), r = this.md5_gg(r, n, i, o, e[a + 1], 5, -165796510), o = this.md5_gg(o, r, n, i, e[a + 6], 9, -1069501632), i = this.md5_gg(i, o, r, n, e[a + 11], 14, 643717713), n = this.md5_gg(n, i, o, r, e[a + 0], 20, -373897302), r = this.md5_gg(r, n, i, o, e[a + 5], 5, -701558691), o = this.md5_gg(o, r, n, i, e[a + 10], 9, 38016083), i = this.md5_gg(i, o, r, n, e[a + 15], 14, -660478335), n = this.md5_gg(n, i, o, r, e[a + 4], 20, -405537848), r = this.md5_gg(r, n, i, o, e[a + 9], 5, 568446438), o = this.md5_gg(o, r, n, i, e[a + 14], 9, -1019803690), i = this.md5_gg(i, o, r, n, e[a + 3], 14, -187363961), n = this.md5_gg(n, i, o, r, e[a + 8], 20, 1163531501), r = this.md5_gg(r, n, i, o, e[a + 13], 5, -1444681467), o = this.md5_gg(o, r, n, i, e[a + 2], 9, -51403784), i = this.md5_gg(i, o, r, n, e[a + 7], 14, 1735328473), n = this.md5_gg(n, i, o, r, e[a + 12], 20, -1926607734), r = this.md5_hh(r, n, i, o, e[a + 5], 4, -378558), o = this.md5_hh(o, r, n, i, e[a + 8], 11, -2022574463), i = this.md5_hh(i, o, r, n, e[a + 11], 16, 1839030562), n = this.md5_hh(n, i, o, r, e[a + 14], 23, -35309556), r = this.md5_hh(r, n, i, o, e[a + 1], 4, -1530992060), o = this.md5_hh(o, r, n, i, e[a + 4], 11, 1272893353), i = this.md5_hh(i, o, r, n, e[a + 7], 16, -155497632), n = this.md5_hh(n, i, o, r, e[a + 10], 23, -1094730640), r = this.md5_hh(r, n, i, o, e[a + 13], 4, 681279174), o = this.md5_hh(o, r, n, i, e[a + 0], 11, -358537222), i = this.md5_hh(i, o, r, n, e[a + 3], 16, -722521979), n = this.md5_hh(n, i, o, r, e[a + 6], 23, 76029189), r = this.md5_hh(r, n, i, o, e[a + 9], 4, -640364487), o = this.md5_hh(o, r, n, i, e[a + 12], 11, -421815835), i = this.md5_hh(i, o, r, n, e[a + 15], 16, 530742520), n = this.md5_hh(n, i, o, r, e[a + 2], 23, -995338651), r = this.md5_ii(r, n, i, o, e[a + 0], 6, -198630844), o = this.md5_ii(o, r, n, i, e[a + 7], 10, 1126891415), i = this.md5_ii(i, o, r, n, e[a + 14], 15, -1416354905), n = this.md5_ii(n, i, o, r, e[a + 5], 21, -57434055), r = this.md5_ii(r, n, i, o, e[a + 12], 6, 1700485571), o = this.md5_ii(o, r, n, i, e[a + 3], 10, -1894986606), i = this.md5_ii(i, o, r, n, e[a + 10], 15, -1051523), n = this.md5_ii(n, i, o, r, e[a + 1], 21, -2054922799), r = this.md5_ii(r, n, i, o, e[a + 8], 6, 1873313359), o = this.md5_ii(o, r, n, i, e[a + 15], 10, -30611744), i = this.md5_ii(i, o, r, n, e[a + 6], 15, -1560198380), n = this.md5_ii(n, i, o, r, e[a + 13], 21, 1309151649), r = this.md5_ii(r, n, i, o, e[a + 4], 6, -145523070), o = this.md5_ii(o, r, n, i, e[a + 11], 10, -1120210379), i = this.md5_ii(i, o, r, n, e[a + 2], 15, 718787259), n = this.md5_ii(n, i, o, r, e[a + 9], 21, -343485551), r = this.safe_add(r, d), n = this.safe_add(n, s), i = this.safe_add(i, c), o = this.safe_add(o, u) + } + return Array(r, n, i, o) + }, + md5_cmn: function(e, t, r, n, i, o) { + return this.safe_add(this.bit_rol(this.safe_add(this.safe_add(t, e), this.safe_add(n, o)), i), r) + }, + md5_ff: function(e, t, r, n, i, o, a) { + return this.md5_cmn(t & r | ~t & n, e, t, i, o, a) + }, + md5_gg: function(e, t, r, n, i, o, a) { + return this.md5_cmn(t & n | r & ~n, e, t, i, o, a) + }, + md5_hh: function(e, t, r, n, i, o, a) { + return this.md5_cmn(t ^ r ^ n, e, t, i, o, a) + }, + md5_ii: function(e, t, r, n, i, o, a) { + return this.md5_cmn(r ^ (t | ~n), e, t, i, o, a) + }, + safe_add: function(e, t) { + var r = (65535 & e) + (65535 & t); + return (e >> 16) + (t >> 16) + (r >> 16) << 16 | 65535 & r + }, + bit_rol: function(e, t) { + return e << t | e >>> 32 - t + }, + str2binl: function(e) { + for (var t = Array(), r = (1 << this.chrsz) - 1, n = 0; n < e.length * this.chrsz; n += this.chrsz) t[n >> 5] |= (e.charCodeAt(n / this.chrsz) & r) << n % 32; + return t + }, + binl2hex: function(e) { + for (var t = "0123456789abcdef", r = "", n = 0; n < 4 * e.length; n++) r += t.charAt(e[n >> 2] >> n % 4 * 8 + 4 & 15) + t.charAt(e[n >> 2] >> n % 4 * 8 & 15); + return r + } + } + + return MD5.hex_md5(inputString); +} + + +hashTaintTest = (hashFunction) => { + var s = randomTaintedString() + randomTaintedString() + randomTaintedString(); + assertTainted(hashFunction(s)); +} + +noHashtest = () => { + hashTaintTest(noHash); +} + +simpleHashtest = () => { + hashTaintTest(simpleHash); +} + +md5Hashtest = () => { + hashTaintTest(md5Hash); +} + +fingerprintHashtest = () => { + hashTaintTest(fingerprintJsHash); +} + +fpFlow4HashTest = () => { + hashTaintTest(fpFlow4Hash); +} + +runTaintTest(noHashtest); +runTaintTest(simpleHashtest); +runTaintTest(md5Hashtest); +runTaintTest(fingerprintHashtest); +runTaintTest(fpFlow4HashTest); + + +if (typeof reportCompare === 'function') + reportCompare(true, true); diff --git a/js/src/tests/non262/taint/maps.js b/js/src/tests/non262/taint/maps.js new file mode 100644 index 0000000000000..8ee88c1ccc292 --- /dev/null +++ b/js/src/tests/non262/taint/maps.js @@ -0,0 +1,42 @@ +function mapNumberObjectKeyTest() { + let map = new Map(); + let tainted = new Number(42); + let untainted = 42; + let value = "foo"; + map.set(untainted, value); + let ret_val_untainted = map.get(untainted) + let ret_val_tainted = map.get(tainted) + // Rather unintuitively, Number object and primitive keys are treated differently in JavaScript + assertNotEq(ret_val_tainted, ret_val_untainted); +} + +function mapTaintedKeyTest() { + let map = new Map(); + let tainted = taint(42); + let untainted = 42; + let value = "foo"; + map.set(tainted, value); + let ret_val_untainted = map.get(untainted) + let ret_val_tainted = map.get(tainted) + assertEq(ret_val_tainted, ret_val_untainted); +} + +function mapUntaintedKeyTest() { + let map = new Map(); + let tainted = taint(42); + let untainted = 42; + let value = "foo"; + map.set(untainted, value); + let ret_val_untainted = map.get(untainted) + let ret_val_tainted = map.get(tainted) + assertEq(ret_val_tainted, ret_val_untainted); +} + +runTaintTest(mapNumberObjectKeyTest); +runTaintTest(mapTaintedKeyTest); +runTaintTest(mapUntaintedKeyTest); + + +if (typeof reportCompare === 'function') + reportCompare(true, true); + diff --git a/js/src/tests/non262/taint/math.js b/js/src/tests/non262/taint/math.js new file mode 100644 index 0000000000000..f9fd5b7525345 --- /dev/null +++ b/js/src/tests/non262/taint/math.js @@ -0,0 +1,305 @@ +function absNumberTaintingTest(){ + var a = taint(42); + var b = taint(-42); + assertNumberTainted(Math.abs(a)); + assertNumberTainted(Math.abs(b)); + assertEq(42, Math.abs(a)); + assertEq(42, Math.abs(b)); +} + +function acosNumberTaintingTest(){ + var a = taint(0.5); + assertNumberTainted(Math.acos(a)); + assertNear(Math.PI / 3, Math.acos(a)); +} + +function acoshNumberTaintingTest(){ + var a = taint(2); + assertNumberTainted(Math.acosh(a)); + assertNear(1.3169578969248166, Math.acosh(a)); +} + +function asinNumberTaintingTest(){ + var a = taint(0.5); + assertNumberTainted(Math.asin(a)); + assertNear(Math.PI / 6, Math.asin(a)); +} + +function asinhNumberTaintingTest(){ + var a = taint(-1); + assertNumberTainted(Math.asinh(a)); + assertNear(-0.881373587019543, Math.asinh(a)); +} + +function atanNumberTaintingTest(){ + var a = taint(1); + assertNumberTainted(Math.atan(a)); + assertNear(Math.PI / 4, Math.atan(a)); +} + +function atan2NumberTaintingTest(){ + var a = taint(10); + var b = taint(0); + var c = 0; + var d = 10; + assertNumberTainted(Math.atan2(a, b)); + assertNumberTainted(Math.atan2(a, c)); + assertNumberTainted(Math.atan2(d, b)); + assertNear(Math.PI / 2, Math.atan2(a, b)); +} + +function atanhNumberTaintingTest(){ + var a = taint(0.5); + assertNumberTainted(Math.atanh(a)); + assertNear(0.5493061443340548, Math.atanh(a)); +} + +function cbrtNumberTaintingTest(){ + var a = taint(64); + assertNumberTainted(Math.cbrt(a)); + assertEq(4, Math.cbrt(a)); +} + +function ceilNumberTaintingTest(){ + var a = taint(1.5); + assertNumberTainted(Math.ceil(a)); + assertEq(2, Math.ceil(a)); +} + +function clz32NumberTaintingTest(){ + var a = taint(1000); + assertNumberTainted(Math.clz32(a)); + assertEq(22, Math.clz32(a)); +} + +function cosNumberTaintingTest(){ + var a = taint(Math.PI / 3); + assertNumberTainted(Math.cos(a)); + assertNear(0.5, Math.cos(a)); +} + +function coshNumberTaintingTest(){ + var a = taint(2); + assertNumberTainted(Math.cosh(a)); + assertNear(3.7621956910836314, Math.cosh(a)); +} + +function expNumberTaintingTest(){ + var a = taint(1); + assertNumberTainted(Math.exp(a)); + assertEq(Math.E, Math.exp(a)); +} + +function expm1NumberTaintingTest(){ + var a = taint(1); + assertNumberTainted(Math.expm1(a)); + assertEq(Math.E - 1, Math.expm1(a)); +} + +function floorNumberTaintingTest(){ + var a = taint(1.5); + assertNumberTainted(Math.floor(a)); + assertEq(1, Math.floor(a)); +} + +function froundNumberTaintingTest(){ + var a = taint(1.337); + assertNumberTainted(Math.fround(a)); + assertEq(Math.fround(1.337), Math.fround(a)); +} + +function hypotNumberTaintingTest(){ + var a = taint(3); + var b = taint(4); + var c = taint(12); + var d = taint(5); + var e = 3; + var f = 12; + + assertNumberTainted(Math.hypot(a, b)); + assertEq(5, Math.hypot(a, b)); + + assertNumberTainted(Math.hypot(a, b, c)); + assertEq(13, Math.hypot(a, b, c)); + + assertNumberTainted(Math.hypot(e, b)); + assertEq(5, Math.hypot(e, b)); + + assertNumberTainted(Math.hypot(e, b, f)); + assertEq(13, Math.hypot(e, b, f)); + + assertNumberTainted(Math.hypot(a, b, c, d)); + assertEq(13.92838827718412, Math.hypot(a, b, c, d)); +} + +function imulNumberTaintingTest(){ + var a = taint(2); + var b = taint(3); + + assertNumberTainted(Math.imul(a, b)); + assertEq(6, Math.imul(a, b)); + + assertNumberTainted(Math.imul(a, 0)); + assertNumberTainted(Math.imul(0, a)); + assertEq(0, Math.imul(a, 0)); + + var d = taint(-2); + var e = taint(-3); + assertNumberTainted(Math.imul(d, e)); + assertEq(6, Math.imul(d, e)); + + // Edge cases with large numbers + var f = taint(0x7fffffff); // Max positive 32-bit signed integer + var g = taint(0x80000000); // Min negative 32-bit signed integer + assertNumberTainted(Math.imul(f, g)); + assertEq(-0x80000000, Math.imul(f, g)); +} + +function logNumberTaintingTest(){ + var a = taint(Math.E); + assertNumberTainted(Math.log(a)); + assertEq(1, Math.log(a)); +} + +function log10NumberTaintingTest(){ + var a = taint(100); + assertNumberTainted(Math.log10(a)); + assertEq(2, Math.log10(a)); +} + +function log1pNumberTaintingTest(){ + var a = taint(100); + assertNumberTainted(Math.log1p(a)); + assertEq(4.61512051684126, Math.log1p(a)); +} + +function log2NumberTaintingTest(){ + var a = taint(8); + assertNumberTainted(Math.log2(a)); + assertEq(3, Math.log2(a)); +} + +function maxNumberTaintingTest(){ + var a = taint(10); + var b = taint(20); + var c = 30; + var d = 0; + assertNumberTainted(Math.max(a, b)); + assertNumberTainted(Math.max(a, d)); + assertNumberTainted(Math.max(a, d, b)); + assertNumberNotTainted(Math.max(a, c)); + assertNumberNotTainted(Math.max(c, b, d)); + assertEq(20, Math.max(a, b)); +} + +function minNumberTaintingTest(){ + var a = taint(10); + var b = taint(20); + var c = 30; + var d = 0; + assertNumberTainted(Math.min(a, b)); + assertNumberTainted(Math.min(a, c)); + assertNumberTainted(Math.min(b, c, a)); + assertNumberNotTainted(Math.min(a, d)); + assertNumberNotTainted(Math.min(c, b, d)); + assertEq(10, Math.min(a, b)); +} + +function powNumberTaintingTest(){ + var a = taint(2); + var b = taint(3); + var c = 2; + var d = 3; + assertNumberTainted(Math.pow(a, b)); + assertNumberTainted(Math.pow(a, d)); + assertNumberTainted(Math.pow(c, b)); + assertEq(8, Math.pow(a, b)); +} + +function roundNumberTaintingTest(){ + var a = taint(1.5); + assertNumberTainted(Math.round(a)); + assertEq(2, Math.round(a)); +} + +function signNumberTaintingTest(){ + var a = taint(42); + var b = taint(-42); + assertNumberTainted(Math.sign(a)); + assertEq(1, Math.sign(a)); + assertNumberTainted(Math.sign(b)); + assertEq(-1, Math.sign(b)); +} + +function sinNumberTaintingTest(){ + var a = taint(Math.PI / 2); + assertNumberTainted(Math.sin(a)); + assertEq(1, Math.sin(a)); +} + +function sinhNumberTaintingTest(){ + var a = taint(2); + assertNumberTainted(Math.sinh(a)); + assertEq(3.626860407847019, Math.sinh(a)); +} + +function sqrtNumberTaintingTest(){ + var a = taint(4); + assertNumberTainted(Math.sqrt(a)); + assertEq(2, Math.sqrt(a)); +} + +function tanNumberTaintingTest(){ + var a = taint(Math.PI / 4); + assertNumberTainted(Math.tan(a)); + assertNear(1, Math.tan(a)); +} + +function tanhNumberTaintingTest(){ + var a = taint(-1); + assertNumberTainted(Math.tanh(a)); + assertNear(-0.7615941559557649, Math.tanh(a)); +} + +function truncNumberTaintingTest(){ + var a = taint(42.43); + assertNumberTainted(Math.trunc(a)); + assertEq(42, Math.trunc(a)); +} + +runTaintTest(absNumberTaintingTest); +runTaintTest(acosNumberTaintingTest); +runTaintTest(acoshNumberTaintingTest); +runTaintTest(asinNumberTaintingTest); +runTaintTest(asinhNumberTaintingTest); +runTaintTest(atanNumberTaintingTest); +runTaintTest(atan2NumberTaintingTest); +runTaintTest(atanhNumberTaintingTest); +runTaintTest(cbrtNumberTaintingTest); +runTaintTest(ceilNumberTaintingTest); +runTaintTest(clz32NumberTaintingTest); +runTaintTest(cosNumberTaintingTest); +runTaintTest(coshNumberTaintingTest); +runTaintTest(expNumberTaintingTest); +runTaintTest(expm1NumberTaintingTest); +runTaintTest(floorNumberTaintingTest); +runTaintTest(froundNumberTaintingTest); +runTaintTest(hypotNumberTaintingTest); +runTaintTest(imulNumberTaintingTest); +runTaintTest(logNumberTaintingTest); +runTaintTest(log10NumberTaintingTest); +runTaintTest(log1pNumberTaintingTest); +runTaintTest(log2NumberTaintingTest); +runTaintTest(maxNumberTaintingTest); +runTaintTest(minNumberTaintingTest); +runTaintTest(powNumberTaintingTest); +runTaintTest(roundNumberTaintingTest); +runTaintTest(signNumberTaintingTest); +runTaintTest(sinNumberTaintingTest); +runTaintTest(sinhNumberTaintingTest); +runTaintTest(sqrtNumberTaintingTest); +runTaintTest(tanNumberTaintingTest); +runTaintTest(tanhNumberTaintingTest); +runTaintTest(truncNumberTaintingTest); + +reportCompare(0, 0, "ok"); \ No newline at end of file diff --git a/js/src/tests/non262/taint/number_string_conversions.js b/js/src/tests/non262/taint/number_string_conversions.js new file mode 100644 index 0000000000000..87f2098433076 --- /dev/null +++ b/js/src/tests/non262/taint/number_string_conversions.js @@ -0,0 +1,68 @@ +function numberStringConversionTests() { + // Test single character taint propagation + var taintedStr = randomTaintedString(); + assertNumberTainted(taintedStr.charCodeAt(0)); + assertTainted(String.fromCharCode(taintedStr.charCodeAt(0))); + + // Test multi character taint propagation + taintedStr = randomMultiTaintedString(); + var chars = []; + for (var i = 0; i < taintedStr.length; i++) { + chars.push(taintedStr.charCodeAt(i)); + } + + var copiedString = String.fromCharCode.apply(null, chars); + assertEqualTaint(copiedString, taintedStr); + + // Test number as parameter taint propagation + index = taint(3) + untaintedStr = "Hello World" + assertTainted(untaintedStr.charAt(index)) + + // Small numbers <=6bit are sometimes transformed to Atoms, when + // converted to strings, which can cause problems + var taintedNumberSmall = taint(42); + var taintedNumberLarge = taint(9000); + + // Text JSON.stringify with inputs including tainted numbers + assertTainted(JSON.stringify(taintedNumberSmall)) + assertTainted(JSON.stringify([1,2,3,taintedNumberSmall,5])) + assertTainted(JSON.stringify({"num":taintedNumberSmall})) + assertTainted(JSON.stringify([1,2,3,{"num":taintedNumberSmall},5])) + + // Test explicit string conversion + assertTainted(taintedNumberLarge.toString()); + assertTainted(String(taintedNumberLarge)); + assertTainted(taintedNumberSmall.toString()); + assertTainted(String(taintedNumberSmall)); + + // Test implicit string conversion + assertTainted(taintedNumberLarge + "abc"); + assertTainted((taintedNumberLarge + "abc")[1]); + assertNotTainted((taintedNumberLarge + "abc")[5]); + assertTainted(taintedNumberSmall + "abc"); + assertTainted((taintedNumberSmall + "abc")[1]); + assertNotTainted((taintedNumberSmall + "abc")[2]); + + // Test explicit number conversion + assertNumberTainted(Number(taint("42"))); + assertNumberTainted(parseInt(taint("42"))); + assertNumberTainted(parseFloat(taint("42.42"))); + + // Test implicit number conversion + assertNumberTainted(42 - taint("2")); + assertNumberTainted(taint("42") - 2); + assertNumberTainted(taint("42") - taint("2")); + assertNumberTainted(42 * taint("2")); + assertNumberTainted(taint("42") * 2); + assertNumberTainted(taint("42") * taint("2")); + assertNumberTainted(42 / taint("2")); + assertNumberTainted(taint("42") / 2); + assertNumberTainted(taint("42") / taint("2")); + +} + +runTaintTest(numberStringConversionTests); + +if (typeof reportCompare === 'function') + reportCompare(true, true); diff --git a/js/src/tests/non262/taint/number_tainting.js b/js/src/tests/non262/taint/number_tainting.js new file mode 100644 index 0000000000000..7ab4512f573f9 --- /dev/null +++ b/js/src/tests/non262/taint/number_tainting.js @@ -0,0 +1,123 @@ +function numberTaintingTest() { + var a = taint(42); + var b = taint(13.37); + assertNumberTainted(a); + assertNumberTainted(b); + + // + // Basic arithmetic tests + assertNumberTainted(a + 1); + + assertNumberTainted(a + 13.37); + assertNumberTainted(13.37 + a); + assertNumberTainted(a + b); + assertEq(a + b, 42 + 13.37); // assertEq uses === for comparing values + assertEq(a + b == 42 + 13.37, true); + + assertNumberTainted(a - 13.37); + assertNumberTainted(13.37 - a); + assertNumberTainted(a - b); + assertEq(a - b, 42 - 13.37); + assertEq(a - b == 42 - 13.37, true); + + assertNumberTainted(a * 13.37); + assertEq(a * 13.37, 561.54); + assertNumberTainted(13.37 * a); + assertEq(13.37 * a, 13.37 * 42); + assertNumberTainted(a * b); + assertEq(a * b, 42 * 13.37); + assertEq(a * b == 42 * 13.37, true); + + assertNumberTainted(a / 13.37); + assertNumberTainted(13.37 / a); + assertNumberTainted(a / b); + assertEq(a / b, 42 / 13.37); + assertEq(a / b == 42 / 13.37, true); + + assertNumberTainted(a % 13.37); + assertNumberTainted(a % b); + assertEq(a % b, 42 % 13.37); + assertEq(a % b == 42 % 13.37, true); + + assertNumberTainted(a ** 13.37); + assertNumberTainted(13.37 ** a); + assertNumberTainted(a ** b); + assertEq(a ** b, 42 ** 13.37); + assertEq(a ** b == 42 ** 13.37, true); + +} + +function incrementNumberTaintingTest() { + + // Number increment/decrement + var a = taint(42); + assertNumberTainted(a); + assertNumberTainted(a++); + assertEq(43, a); + assertNumberTainted(a--); + assertEq(42, a); + assertNumberTainted(a); + assertNumberTainted(++a); + assertEq(43, a); + assertNumberTainted(a); + assertNumberTainted(--a); + assertEq(42, a); + assertNumberTainted(a); + + // Number increment/decrement + var b = taint(3.14159); + assertNumberTainted(b); + assertNumberTainted(b++); + assertNumberTainted(b--); + assertNumberTainted(b); + assertNumberTainted(++b); + assertNumberTainted(b); + assertNumberTainted(--b); + assertNumberTainted(b); + +} + +function bitwiseNumberTaintingTest() { + // + // Bitwise operations + var a = taint(42); + var b = taint(3); + assertNumberTainted(a << 1); + assertNumberTainted(a << b); + assertEq(a << b, 42 << 3); + + assertNumberTainted(a >> 1); + assertNumberTainted(a >> b); + assertEq(a >> b, 42 >> 3); + + assertNumberTainted(a >>> 1); + assertNumberTainted(a >>> b); + assertEq(a >> b, 42 >>> 3); + assertEq(taint(-5) >>> taint(2), 1073741822); + + assertNumberTainted(a & 1); + assertNumberTainted(a & b); + assertEq(a & b, 42 & 3); + + assertNumberTainted(a | 1); + assertNumberTainted(a | b); + assertEq(a | b, 42 | 3); + + assertNumberTainted(a ^ 1); + assertNumberTainted(a ^ b); + assertEq(a ^ b, 42 ^ 3); + + assertNumberTainted(~a); + assertEq(~a, ~42); + + // Element access + var table = [0,1,2,3,4,5,6,7]; + assertNumberTainted(table[a & 7]); +} + +runTaintTest(numberTaintingTest); +runTaintTest(incrementNumberTaintingTest); +runTaintTest(bitwiseNumberTaintingTest); + +if (typeof reportCompare === 'function') + reportCompare(true, true); diff --git a/js/src/tests/non262/taint/number_tainting_breaking_features.js b/js/src/tests/non262/taint/number_tainting_breaking_features.js new file mode 100644 index 0000000000000..7d4d34bd2e6ec --- /dev/null +++ b/js/src/tests/non262/taint/number_tainting_breaking_features.js @@ -0,0 +1,116 @@ +// During the development of number tainting, some JavaScript features got +// broken. This led to some pages not loading correctly. +// In an effort to fixe these errors, this test explicity tests the currently +// or previously broken functionality to ensure it does not get broken again. + +// Test a function using a primitive, a tainted number (object) and an untainted +// number object +function testPrimitiveTaintedObject(numberValue, expectedPrimitive, expectedTainted, expectedNumber, testFunc) { + let primitive = numberValue + let tainted = taint(numberValue) + let number = new Number(numberValue) + testFunc(primitive, expectedPrimitive) + testFunc(tainted, expectedTainted) + testFunc(number, expectedNumber) +} + + +function numberTaintingBreakingFeatures() { + + var a = taint(42); + var b = taint(13.37); + + // Addition using arrays + assertEq([1] + 1, '11') + assertEq([12345] + 6789, '123456789') + + + // Is Integer + testPrimitiveTaintedObject(123, true, true, false, (value, expected) => { + assertEq(Number.isInteger(value), expected) + }) + + testPrimitiveTaintedObject(123.456, false, false, false, (value, expected) => { + assertEq(Number.isInteger(value), expected) + }) + + + // IsSafeInteger + testPrimitiveTaintedObject(123, true, true, false, (value, expected) => { + assertEq(Number.isSafeInteger(value), expected) + }) + + testPrimitiveTaintedObject(2 ** 53 - 1, true, true, false, (value, expected) => { + assertEq(Number.isSafeInteger(value), expected) + }) + + testPrimitiveTaintedObject(NaN, false, false, false, (value, expected) => { + assertEq(Number.isSafeInteger(value), expected) + }) + + testPrimitiveTaintedObject(Infinity, false, false, false, (value, expected) => { + assertEq(Number.isSafeInteger(value), expected) + }) + + testPrimitiveTaintedObject("3", false, false, false, (value, expected) => { + assertEq(Number.isSafeInteger(value), expected) + }) + + testPrimitiveTaintedObject(3.1, false, false, false, (value, expected) => { + assertEq(Number.isSafeInteger(value), expected) + }) + + + // IsFinite + testPrimitiveTaintedObject(2531,true,true,false,(value,expected)=>{ + assertEq(Number.isFinite(value), expected) + }) + + testPrimitiveTaintedObject(Infinity, false, false, false, (value, expected) => { + assertEq(Number.isFinite(value), expected) + }) + + testPrimitiveTaintedObject(NaN, false, false, false, (value, expected) => { + assertEq(Number.isFinite(value), expected) + }) + + testPrimitiveTaintedObject(-Infinity, false, false, false, (value, expected) => { + assertEq(Number.isFinite(value), expected) + }) + + + // IsNaN + testPrimitiveTaintedObject(NaN,true,true,false,(value,expected)=>{ + assertEq(Number.isNaN(value), expected) + }) + + testPrimitiveTaintedObject(Number.NaN,true,true,false,(value,expected)=>{ + assertEq(Number.isNaN(value), expected) + }) + + testPrimitiveTaintedObject(2531, false, false, false, (value, expected) => { + assertEq(Number.isNaN(value), expected) + }) + + // typeof + testPrimitiveTaintedObject(2531, 'number', 'number', 'object', (value, expected) => { + assertEq(typeof value, expected) + }) + + testPrimitiveTaintedObject(2531, 'number', 'number', 'object', (value, expected) => { + assertEq(typeof(value), expected) + }) + + testPrimitiveTaintedObject(1, 'number', 'number', 'object', (value, expected) => { + assertEq(typeof(value), expected) + }) + + + + +} + +runTaintTest(numberTaintingBreakingFeatures); + +if (typeof reportCompare === 'function') + reportCompare(true, true); diff --git a/js/src/tests/non262/taint/shell.js b/js/src/tests/non262/taint/shell.js index 503e7b0c9eac8..51d2da4b19bb7 100644 --- a/js/src/tests/non262/taint/shell.js +++ b/js/src/tests/non262/taint/shell.js @@ -222,22 +222,24 @@ if (typeof assertNotHasTaintOperation === 'undefined') { if (typeof assertLastTaintOperationEquals === 'undefined') { var assertLastTaintOperationEquals = function(str, opName) { + var lastOp = "Unknown"; for (var i = 0; i < str.taint.length; i++) { var range = str.taint[i]; // Quirk: ignore "function call arguments" nodes for now... var index = 0; while (index < range.flow.length && - range.flow[index].operation == "function" && - range.flow[index].arguments[0].startsWith("assert")) + range.flow[index].operation == "function" && + range.flow[index].arguments[0].startsWith("assert")) index++; var node = range.flow[index]; + lastOp = node.operation; if (node.operation === opName) { return true; } } - throw Error("String '" + str + "' does not contain \"" + opName + "\" as last taint operation. Taint: " + JSON.stringify(str.taint)); + throw Error("String '" + str + "' does not contain \"" + opName + "\" as last taint operation (\"" + lastOp + "\"). Taint: " + JSON.stringify(str.taint)); } } @@ -247,7 +249,7 @@ if (typeof runTaintTest === 'undefined') { // Separate function so it's visible in the backtrace var runJITTest = function(doTest) { // Force JIT compilation - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { //console.log(i); doTest(); } @@ -257,3 +259,96 @@ if (typeof runTaintTest === 'undefined') { runJITTest(doTest); } } + +if (typeof assertNumberTainted === 'undefined') { + // Assert that the given number is tainted. + var assertNumberTainted = function(num) { + if (!num.taint || num.taint.length == 0) { + throw Error("Number (" + num + ") is not tainted"); + } + } +} + +if (typeof assertNumberNotTainted === 'undefined') { + // Assert that the given number is not tainted. + var assertNumberNotTainted = function(num) { + if (num.taint && num.taint.length > 0) { + throw Error("Number (" + num + ") is tainted"); + } + } +} + +// The nearest representable values to +1.0. +const ONE_PLUS_EPSILON = 1 + Math.pow(2, -52); // 0.9999999999999999 +const ONE_MINUS_EPSILON = 1 - Math.pow(2, -53); // 1.0000000000000002 + +{ + const fail = function (msg) { + var exc = new Error(msg); + try { + // Try to improve on exc.fileName and .lineNumber; leave exc.stack + // alone. We skip two frames: fail() and its caller, an assertX() + // function. + var frames = exc.stack.trim().split("\n"); + if (frames.length > 2) { + var m = /@([^@:]*):([0-9]+)$/.exec(frames[2]); + if (m) { + exc.fileName = m[1]; + exc.lineNumber = +m[2]; + } + } + } catch (ignore) { throw ignore;} + throw exc; + }; + + let ENDIAN; // 0 for little-endian, 1 for big-endian. + + // Return the difference between the IEEE 754 bit-patterns for a and b. + // + // This is meaningful when a and b are both finite and have the same + // sign. Then the following hold: + // + // * If a === b, then diff(a, b) === 0. + // + // * If a !== b, then diff(a, b) === 1 + the number of representable values + // between a and b. + // + const f = new Float64Array([0, 0]); + const u = new Uint32Array(f.buffer); + const diff = function (a, b) { + f[0] = a; + f[1] = b; + //print(u[1].toString(16) + u[0].toString(16) + " " + u[3].toString(16) + u[2].toString(16)); + return Math.abs((u[3-ENDIAN] - u[1-ENDIAN]) * 0x100000000 + u[2+ENDIAN] - u[0+ENDIAN]); + }; + + // Set ENDIAN to the platform's endianness. + ENDIAN = 0; // try little-endian first + if (diff(2, 4) === 0x100000) // exact wrong answer we'll get on a big-endian platform + ENDIAN = 1; + assertEq(diff(2,4), 0x10000000000000); + assertEq(diff(0, Number.MIN_VALUE), 1); + assertEq(diff(1, ONE_PLUS_EPSILON), 1); + assertEq(diff(1, ONE_MINUS_EPSILON), 1); + + var assertNear = function assertNear(a, b, tolerance=1) { + if (!Number.isFinite(b)) { + fail("second argument to assertNear (expected value) must be a finite number"); + } else if (Number.isNaN(a)) { + fail("got NaN, expected a number near " + b); + } else if (!Number.isFinite(a)) { + if (b * Math.sign(a) < Number.MAX_VALUE) + fail("got " + a + ", expected a number near " + b); + } else { + // When the two arguments do not have the same sign bit, diff() + // returns some huge number. So if b is positive or negative 0, + // make target the zero that has the same sign bit as a. + var target = b === 0 ? a * 0 : b; + var err = diff(a, target); + if (err > tolerance) { + fail("got " + a + ", expected a number near " + b + + " (relative error: " + err + ")"); + } + } + }; +} diff --git a/js/src/tests/non262/taint/switch-case.js b/js/src/tests/non262/taint/switch-case.js new file mode 100644 index 0000000000000..e292d4810a343 --- /dev/null +++ b/js/src/tests/non262/taint/switch-case.js @@ -0,0 +1,28 @@ +function taintSwitchCaseTest() { + + let tableSwitchOptimized = 1; + let tableSwitchOptimizedTainted = Number.tainted(tableSwitchOptimized); + + switch (tableSwitchOptimizedTainted) { + case tableSwitchOptimized: + break; + default: + throw new Error("Taints should not be propagated in optimized switch cases"); + } + + let tableSwitchUnoptimized = 123456789; + let tableSwitchUnoptimizedTainted = Number.tainted(tableSwitchUnoptimized); + + switch (tableSwitchUnoptimizedTainted) { + case tableSwitchUnoptimized: + break; + default: + throw new Error("Taints should not be propagated in optimized switch cases"); + } +} + +runTaintTest(taintSwitchCaseTest); + +if (typeof reportCompare === 'function') + reportCompare(true, true); + diff --git a/js/src/tests/shell.js b/js/src/tests/shell.js index fbc7dc102c732..efd1f0b91eb62 100644 --- a/js/src/tests/shell.js +++ b/js/src/tests/shell.js @@ -131,6 +131,17 @@ global.assertEq = assertEq; } + var assertNotEq = global.assertNotEq; + if (typeof assertNotEq !== "function") { + assertNotEq = function assertNotEq(actual, expected, message) { + if (SameValue(actual, expected)) { + throw new TypeError(`Assertion failed: got "${actual}", expected "${expected}"` + + (message ? ": " + message : "")); + } + }; + global.assertNotEq = assertNotEq; + } + function assertEqArray(actual, expected) { var len = actual.length; assertEq(len, expected.length, "mismatching array lengths"); diff --git a/js/src/vm/EqualityOperations.cpp b/js/src/vm/EqualityOperations.cpp index 66438879ce9c2..34d42435f24de 100644 --- a/js/src/vm/EqualityOperations.cpp +++ b/js/src/vm/EqualityOperations.cpp @@ -236,6 +236,18 @@ bool js::StrictlyEqual(JSContext* cx, JS::Handle lval, return true; } + // TaintFox: special case to handle strict equality of tainted numbers. + if (isAnyTaintedNumber(lval, rval) && + (lval.isNumber() || isTaintedNumber(lval)) && + (rval.isNumber() || isTaintedNumber(rval))) { + double l, r; + if (!ToNumber(cx, lval, &l) || !ToNumber(cx, rval, &r)) + return false; + + *equal = (l == r); + return true; + } + *equal = false; return true; } diff --git a/js/src/vm/EqualityOperations.h b/js/src/vm/EqualityOperations.h index f08f179730472..2b92c9ad5e488 100644 --- a/js/src/vm/EqualityOperations.h +++ b/js/src/vm/EqualityOperations.h @@ -18,6 +18,7 @@ #ifndef vm_EqualityOperations_h #define vm_EqualityOperations_h +#include "jstaint.h" // JS::isTaintedNUmber #include "jstypes.h" // JS_PUBLIC_API #include "js/RootingAPI.h" // JS::Handle #include "js/Value.h" // JS::Value @@ -64,7 +65,7 @@ extern bool SameValueZero(JSContext* cx, JS::Handle v1, * integers too. */ inline bool CanUseBitwiseCompareForStrictlyEqual(const JS::Value& v) { - return v.isObject() || v.isSymbol() || v.isNullOrUndefined() || v.isBoolean(); + return (v.isObject() || v.isSymbol() || v.isNullOrUndefined() || v.isBoolean()) && (!JS::isTaintedNumber(v)); } } // namespace js diff --git a/js/src/vm/Interpreter-inl.h b/js/src/vm/Interpreter-inl.h index d644e755a7244..fc56a403fe69c 100644 --- a/js/src/vm/Interpreter-inl.h +++ b/js/src/vm/Interpreter-inl.h @@ -32,6 +32,7 @@ #include "vm/JSContext-inl.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" +#include "vm/NumberObject-inl.h" #include "vm/ObjectOperations-inl.h" #include "vm/StringType-inl.h" @@ -309,6 +310,15 @@ static MOZ_ALWAYS_INLINE bool NegOperation(JSContext* cx, * INT32_FITS_IN_JSVAL(-i) unless i is 0 or INT32_MIN when the * results, -0.0 or INT32_MAX + 1, are double values. */ + // TaintFox: handle tainted numbers + if (isTaintedNumber(val)) { + double d; + if (!ToNumber(cx, val, &d)) + return false; + res.setObject(*NumberObject::createTainted(cx, -d, getNumberTaint(val))); + return true; + } + int32_t i; if (val.isInt32() && (i = val.toInt32()) != 0 && i != INT32_MIN) { res.setInt32(-i); @@ -340,6 +350,15 @@ static MOZ_ALWAYS_INLINE bool IncOperation(JSContext* cx, HandleValue val, return true; } + if (isTaintedNumber(val)) { + double d; + if (!ToNumber(cx, val, &d)) { + return false; + } + res.setObject(*NumberObject::createTainted(cx, d + 1, getNumberTaint(val))); + return true; + } + MOZ_ASSERT(val.isBigInt(), "+1 only callable on result of JSOp::ToNumeric"); return BigInt::incValue(cx, val, res); } @@ -357,6 +376,15 @@ static MOZ_ALWAYS_INLINE bool DecOperation(JSContext* cx, HandleValue val, return true; } + if (isTaintedNumber(val)) { + double d; + if (!ToNumber(cx, val, &d)) { + return false; + } + res.setObject(*NumberObject::createTainted(cx, d - 1, getNumberTaint(val))); + return true; + } + MOZ_ASSERT(val.isBigInt(), "-1 only callable on result of JSOp::ToNumeric"); return BigInt::decValue(cx, val, res); } @@ -384,6 +412,10 @@ static MOZ_ALWAYS_INLINE bool GetObjectElementOperation( MOZ_ASSERT(op == JSOp::GetElem || op == JSOp::GetElemSuper); MOZ_ASSERT_IF(op == JSOp::GetElem, obj == &receiver.toObject()); + // TaintFox: tainted numbers or strings might be used for element access. In that case, also + // try to taint the resulting value. + TaintFlow taint; + do { uint32_t index; if (IsDefinitelyIndex(key, &index)) { @@ -399,6 +431,12 @@ static MOZ_ALWAYS_INLINE bool GetObjectElementOperation( if (key.isString()) { JSString* str = key.toString(); + + // TaintFox: if tainted, just pick the first taintflow. + if (str->isTainted()) { + taint = str->taint().begin()->flow(); + } + JSAtom* name = str->isAtom() ? &str->asAtom() : AtomizeString(cx, str); if (!name) { return false; @@ -415,6 +453,10 @@ static MOZ_ALWAYS_INLINE bool GetObjectElementOperation( } } + if (isTaintedNumber(key)) { + taint = getNumberTaint(key); + } + RootedId id(cx); if (!ToPropertyKey(cx, key, &id)) { return false; @@ -424,6 +466,36 @@ static MOZ_ALWAYS_INLINE bool GetObjectElementOperation( } } while (false); + // TaintFox: add taint information to looked up element. + if (taint) { + if (res.isString()) { + // Simple heuristic. In essence we want to apply taint in case of a + // lookup table or similar, since there the resulting string is completely controlled + // if the index is controlled. + // On the other hand, here is an example where we probably don't want to apply taint: + // + // var fortunes = [ //.. array of strings ]; + // function fortune(i) { + // return fortunes[i]; + // } + // + // In this case we aren't able to control the content of the string but only which + // string is choosen, which probably isn't relevant security wise. + // + // Our heuristic here tries to differentiate both cases simply by looking at the length + // of the returned string. + if (res.toString()->length() < 3) { + // We only want to taint the returned string, not the element of the object. + RootedString str(cx, res.toString()); + JSString* tainted_str = NewDependentString(cx, str, 0, str->length()); + tainted_str->setTaint(cx, SafeStringTaint(taint, str->length())); + res.setString(tainted_str); + } + } else if (res.isNumber()) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), taint)); + } + } + cx->debugOnlyCheck(res); return true; } @@ -489,6 +561,17 @@ static MOZ_ALWAYS_INLINE bool GetPrimitiveElementOperation( if (!GetProperty(cx, boxed, receiver, id, res)) { return false; } + + // TaintFox:Like with arrays, taint should be propagated here + // if the index is a tainted number and the receiver a string. + if (isTaintedNumber(key) && res.isString()) { + RootedString str(cx, res.toString()); + SafeStringTaint taint(getNumberTaint(key), str->length()); + JSString* tainted_str = NewDependentString(cx, str, 0, str->length()); + tainted_str->setTaint(cx, taint); + res.setString(tainted_str); + } + } while (false); cx->debugOnlyCheck(res); @@ -658,6 +741,10 @@ static MOZ_ALWAYS_INLINE bool AddOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue res) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + if (lhs.isInt32() && rhs.isInt32()) { int32_t l = lhs.toInt32(), r = rhs.toInt32(); int32_t t; @@ -667,15 +754,38 @@ static MOZ_ALWAYS_INLINE bool AddOperation(JSContext* cx, } } + // TaintFox: Deactivate generic ToPrimitive() without preffered type here. + // This would cause taintedNumber objects to be converted to primitive numbers + // without taint. This is bad, if we later have to convert to String. + // ToString() is internally using ToPrimitive() with a preffered type, which + // supports taint propagation. + /* if (!ToPrimitive(cx, lhs)) { return false; } if (!ToPrimitive(cx, rhs)) { return false; } + */ bool lIsString = lhs.isString(); bool rIsString = rhs.isString(); + + // TaintFox: Cast to primitive if values are neither strings nor tainted numbers + // Required for some special cases like arrays + if (!lIsString && !isTaintedNumber(lhs)) { + if (!ToPrimitive(cx, lhs)) { + return false; + } + lIsString = lhs.isString(); + } + if (!rIsString && !isTaintedNumber(rhs)) { + if (!ToPrimitive(cx, rhs)) { + return false; + } + rIsString = rhs.isString(); + } + if (lIsString || rIsString) { JSString* lstr; if (lIsString) { @@ -711,6 +821,16 @@ static MOZ_ALWAYS_INLINE bool AddOperation(JSContext* cx, return true; } + // TaintFox: If no string conversion of numbers is needed, we can safely + // use the generic ToPrimitive() taint is also lost in this case but will + // later be added based on origLhs and origRhs + if (!ToPrimitive(cx, lhs)) { + return false; + } + if (!ToPrimitive(cx, rhs)) { + return false; + } + if (!ToNumeric(cx, lhs) || !ToNumeric(cx, rhs)) { return false; } @@ -720,6 +840,11 @@ static MOZ_ALWAYS_INLINE bool AddOperation(JSContext* cx, } res.setNumber(lhs.toNumber() + rhs.toNumber()); + + // TaintFox: Taint propagation when adding tainted numbers. + if (isAnyTaintedValue(origLhs, origRhs)) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), getAnyValueTaint(origLhs, origRhs, "+"))); + } return true; } @@ -727,7 +852,11 @@ static MOZ_ALWAYS_INLINE bool SubOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue res) { - if (!ToNumeric(cx, lhs) || !ToNumeric(cx, rhs)) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + + if (!ToNumericUnboxTainted(cx, lhs) || !ToNumericUnboxTainted(cx, rhs)) { return false; } @@ -735,7 +864,13 @@ static MOZ_ALWAYS_INLINE bool SubOperation(JSContext* cx, return BigInt::subValue(cx, lhs, rhs, res); } + + res.setNumber(lhs.toNumber() - rhs.toNumber()); + // TaintFox: Taint propagation when subtracting tainted numbers. + if (isAnyTaintedValue(origLhs, origRhs)) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), getAnyValueTaint(origLhs, origRhs, "-"))); + } return true; } @@ -743,7 +878,11 @@ static MOZ_ALWAYS_INLINE bool MulOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue res) { - if (!ToNumeric(cx, lhs) || !ToNumeric(cx, rhs)) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + + if (!ToNumericUnboxTainted(cx, lhs) || !ToNumericUnboxTainted(cx, rhs)) { return false; } @@ -752,6 +891,10 @@ static MOZ_ALWAYS_INLINE bool MulOperation(JSContext* cx, } res.setNumber(lhs.toNumber() * rhs.toNumber()); + // TaintFox: Taint propagation when multiplying tainted numbers. + if (isAnyTaintedValue(origLhs, origRhs)) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), getAnyValueTaint(origLhs, origRhs, "*"))); + } return true; } @@ -759,7 +902,11 @@ static MOZ_ALWAYS_INLINE bool DivOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue res) { - if (!ToNumeric(cx, lhs) || !ToNumeric(cx, rhs)) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + + if (!ToNumericUnboxTainted(cx, lhs) || !ToNumericUnboxTainted(cx, rhs)) { return false; } @@ -768,6 +915,10 @@ static MOZ_ALWAYS_INLINE bool DivOperation(JSContext* cx, } res.setNumber(NumberDiv(lhs.toNumber(), rhs.toNumber())); + // TaintFox: Taint propagation when dividing tainted numbers. + if (isAnyTaintedValue(origLhs, origRhs)) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), getAnyValueTaint(origLhs, origRhs, "/"))); + } return true; } @@ -775,6 +926,10 @@ static MOZ_ALWAYS_INLINE bool ModOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue res) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + int32_t l, r; if (lhs.isInt32() && rhs.isInt32() && (l = lhs.toInt32()) >= 0 && (r = rhs.toInt32()) > 0) { @@ -783,7 +938,7 @@ static MOZ_ALWAYS_INLINE bool ModOperation(JSContext* cx, return true; } - if (!ToNumeric(cx, lhs) || !ToNumeric(cx, rhs)) { + if (!ToNumericUnboxTainted(cx, lhs) || !ToNumericUnboxTainted(cx, rhs)) { return false; } @@ -792,14 +947,22 @@ static MOZ_ALWAYS_INLINE bool ModOperation(JSContext* cx, } res.setNumber(NumberMod(lhs.toNumber(), rhs.toNumber())); + // TaintFox: Taint propagation when modding tainted numbers. + if (isAnyTaintedNumber(origLhs, origRhs)) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), getAnyNumberTaint(origLhs, origRhs, "%"))); + } return true; } - + static MOZ_ALWAYS_INLINE bool PowOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue res) { - if (!ToNumeric(cx, lhs) || !ToNumeric(cx, rhs)) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + + if (!ToNumericUnboxTainted(cx, lhs) || !ToNumericUnboxTainted(cx, rhs)) { return false; } @@ -808,12 +971,19 @@ static MOZ_ALWAYS_INLINE bool PowOperation(JSContext* cx, } res.setNumber(ecmaPow(lhs.toNumber(), rhs.toNumber())); + // TaintFox: Taint propagation when taking power of tainted numbers. + if (isAnyTaintedNumber(origLhs, origRhs)) { + res.setObject(*NumberObject::createTainted(cx, res.toNumber(), getAnyNumberTaint(origLhs, origRhs, "**"))); + } return true; } static MOZ_ALWAYS_INLINE bool BitNotOperation(JSContext* cx, MutableHandleValue in, MutableHandleValue out) { + // TaintFox: copy in since it is mutable. + RootedValue origIn(cx, in); + if (!ToInt32OrBigInt(cx, in)) { return false; } @@ -823,6 +993,11 @@ static MOZ_ALWAYS_INLINE bool BitNotOperation(JSContext* cx, } out.setInt32(~in.toInt32()); + + // TaintFox: Taint propagation for bitwise not. + if (isTaintedNumber(origIn)) { + out.setObject(*NumberObject::createTainted(cx, out.toInt32(), getNumberTaint(origIn))); + } return true; } @@ -830,6 +1005,10 @@ static MOZ_ALWAYS_INLINE bool BitXorOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue out) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + if (!ToInt32OrBigInt(cx, lhs) || !ToInt32OrBigInt(cx, rhs)) { return false; } @@ -839,6 +1018,11 @@ static MOZ_ALWAYS_INLINE bool BitXorOperation(JSContext* cx, } out.setInt32(lhs.toInt32() ^ rhs.toInt32()); + + // TaintFox: Taint propagation for bitwise xor. + if (isAnyTaintedNumber(origLhs, origRhs)) { + out.setObject(*NumberObject::createTainted(cx, out.toInt32(), getAnyNumberTaint(origLhs, origRhs, "^"))); + } return true; } @@ -846,6 +1030,10 @@ static MOZ_ALWAYS_INLINE bool BitOrOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue out) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + if (!ToInt32OrBigInt(cx, lhs) || !ToInt32OrBigInt(cx, rhs)) { return false; } @@ -855,6 +1043,11 @@ static MOZ_ALWAYS_INLINE bool BitOrOperation(JSContext* cx, } out.setInt32(lhs.toInt32() | rhs.toInt32()); + + // TaintFox: Taint propagation for bitwise or. + if (isAnyTaintedNumber(origLhs, origRhs)) { + out.setObject(*NumberObject::createTainted(cx, out.toInt32(), getAnyNumberTaint(origLhs, origRhs, "|"))); + } return true; } @@ -862,6 +1055,10 @@ static MOZ_ALWAYS_INLINE bool BitAndOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue out) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + if (!ToInt32OrBigInt(cx, lhs) || !ToInt32OrBigInt(cx, rhs)) { return false; } @@ -871,6 +1068,11 @@ static MOZ_ALWAYS_INLINE bool BitAndOperation(JSContext* cx, } out.setInt32(lhs.toInt32() & rhs.toInt32()); + + // TaintFox: Taint propagation for bitwise and. + if (isAnyTaintedNumber(origLhs, origRhs)) { + out.setObject(*NumberObject::createTainted(cx, out.toInt32(), getAnyNumberTaint(origLhs, origRhs, "&"))); + } return true; } @@ -878,6 +1080,10 @@ static MOZ_ALWAYS_INLINE bool BitLshOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue out) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + if (!ToInt32OrBigInt(cx, lhs) || !ToInt32OrBigInt(cx, rhs)) { return false; } @@ -892,6 +1098,11 @@ static MOZ_ALWAYS_INLINE bool BitLshOperation(JSContext* cx, uint32_t left = static_cast(lhs.toInt32()); uint8_t right = rhs.toInt32() & 31; out.setInt32(mozilla::WrapToSigned(left << right)); + + // TaintFox: Taint propagation for bitwise left shift. + if (isAnyTaintedNumber(origLhs, origRhs)) { + out.setObject(*NumberObject::createTainted(cx, out.toInt32(), getAnyNumberTaint(origLhs, origRhs, "<<"))); + } return true; } @@ -899,6 +1110,10 @@ static MOZ_ALWAYS_INLINE bool BitRshOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue out) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + if (!ToInt32OrBigInt(cx, lhs) || !ToInt32OrBigInt(cx, rhs)) { return false; } @@ -908,6 +1123,11 @@ static MOZ_ALWAYS_INLINE bool BitRshOperation(JSContext* cx, } out.setInt32(lhs.toInt32() >> (rhs.toInt32() & 31)); + + // TaintFox: Taint propagation for bitwise right shift. + if (isAnyTaintedNumber(origLhs, origRhs)) { + out.setObject(*NumberObject::createTainted(cx, out.toInt32(), getAnyNumberTaint(origLhs, origRhs, ">>"))); + } return true; } @@ -915,6 +1135,10 @@ static MOZ_ALWAYS_INLINE bool UrshOperation(JSContext* cx, MutableHandleValue lhs, MutableHandleValue rhs, MutableHandleValue out) { + // TaintFox: copy lhs and rhs since they are mutable. + RootedValue origLhs(cx, lhs); + RootedValue origRhs(cx, rhs); + if (!ToNumeric(cx, lhs) || !ToNumeric(cx, rhs)) { return false; } @@ -932,6 +1156,11 @@ static MOZ_ALWAYS_INLINE bool UrshOperation(JSContext* cx, } left >>= right & 31; out.setNumber(uint32_t(left)); + + // TaintFox: Taint propagation for unsigned right shift. + if (isAnyTaintedNumber(origLhs, origRhs)) { + out.setObject(*NumberObject::createTainted(cx, out.toNumber(), getAnyNumberTaint(origLhs, origRhs, ">>"))); + } return true; } diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 8346c1e73f231..d1bfdf1786dfb 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -27,6 +27,7 @@ #include "jslibmath.h" #include "jsmath.h" #include "jsnum.h" +#include "jstaint.h" #include "builtin/Array.h" #include "builtin/Eval.h" @@ -950,6 +951,10 @@ JSType js::TypeOfValue(const Value& v) { case ValueType::Undefined: return JSTYPE_UNDEFINED; case ValueType::Object: + // TaintFox: Hide the fact, that tainted numbers are number objects + if (JS::isTaintedNumber(v)){ + return JSTYPE_NUMBER; + } return TypeOfObject(&v.toObject()); #ifdef ENABLE_RECORD_TUPLE case ValueType::ExtendedPrimitive: @@ -3325,6 +3330,9 @@ bool MOZ_NEVER_INLINE JS_HAZ_JSNATIVE_CALLER js::Interpret(JSContext* cx, int32_t i; if (rref.isInt32()) { i = rref.toInt32(); + } + else if (isTaintedNumber(rref)) { + i = rref.toObject().as().unbox(); } else { /* Use mozilla::NumberEqualsInt32 to treat -0 (double) as 0. */ if (!rref.isDouble() || !NumberEqualsInt32(rref.toDouble(), &i)) { diff --git a/js/src/vm/NumberObject-inl.h b/js/src/vm/NumberObject-inl.h index 4f4123f18db81..21690786130f2 100644 --- a/js/src/vm/NumberObject-inl.h +++ b/js/src/vm/NumberObject-inl.h @@ -20,9 +20,57 @@ inline NumberObject* NumberObject::create(JSContext* cx, double d, return nullptr; } obj->setPrimitiveValue(d); + + // Taintfox: initialize the taint slot to null + obj->initReservedSlot(TAINT_SLOT, PrivateValue(nullptr)); + + bool insideNursery = IsInsideNursery(obj); + if (insideNursery) { + if (!cx->nursery().addNumberObjectWithNurseryMemory(obj)) { + ReportOutOfMemory(cx); + return nullptr; + } + } + return obj; +} + + + +inline NumberObject* +NumberObject::createTainted(JSContext* cx, double d, const TaintFlow& taint, HandleObject proto /* = nullptr */) +{ + NumberObject* obj = create(cx, d, proto); + if (!obj) { + return nullptr; + } + obj->setTaint(taint); + return obj; } -} // namespace js + +inline void NumberObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + NumberObject* numobj = static_cast(obj); + if (obj) { + TaintFlow* flow = numobj->getTaintFlow(); + if (flow) { + delete flow; + numobj->setReservedSlot(TAINT_SLOT, PrivateValue(nullptr)); + } + } +} + + +inline void NumberObject::sweepAfterMinorGC(JS::GCContext* gcx, NumberObject *obj) { + bool wasInsideNursery = IsInsideNursery(obj); + if (wasInsideNursery && !IsForwarded(obj)) { + finalize(gcx, obj); + return; + } +} + + +} // namespace js #endif /* vm_NumberObject_inl_h */ diff --git a/js/src/vm/NumberObject.h b/js/src/vm/NumberObject.h index 936cd9db1da4f..e42d4329e3670 100644 --- a/js/src/vm/NumberObject.h +++ b/js/src/vm/NumberObject.h @@ -7,18 +7,27 @@ #ifndef vm_NumberObject_h #define vm_NumberObject_h +#include + +#include "jsnum.h" +#include "Taint.h" + #include "vm/NativeObject.h" namespace js { +// TaintFox: Number objects can be tainted. class NumberObject : public NativeObject { /* Stores this Number object's [[PrimitiveValue]]. */ static const unsigned PRIMITIVE_VALUE_SLOT = 0; + /* Taintfox: Stores the Number object's taint information */ + static const unsigned TAINT_SLOT = 1; + static const ClassSpec classSpec_; public: - static const unsigned RESERVED_SLOTS = 1; + static const unsigned RESERVED_SLOTS = 2; static const JSClass class_; @@ -31,12 +40,67 @@ class NumberObject : public NativeObject { double unbox() const { return getFixedSlot(PRIMITIVE_VALUE_SLOT).toNumber(); } - private: + static inline NumberObject* createTainted(JSContext* cx, double d, + const TaintFlow& taint, + HandleObject proto = nullptr); + + void finalize(JS::GCContext* gcx) { + NumberObject::finalize(gcx, this); + JSObject::finalize(gcx); + } + + // TaintFox: A finalizer is required for correct memory handling. + static void finalize(JS::GCContext* gcx, JSObject* obj); + + static void sweepAfterMinorGC(JS::GCContext* gcx, NumberObject* numobj); + + const TaintFlow& taint() const { + TaintFlow* flow = getTaintFlow(); + if (flow) { + // head->addref(); + return *flow; + } + return TaintFlow::getEmptyTaintFlow(); + } + + void setTaint(const TaintFlow& taint) { + // TaintNode* current = getTaintNode(); + // if (current) { + // current->release(); + // } + // TaintNode* head = taint.head(); + // if (head) { + // head->addref(); + // } + TaintFlow* flow = getTaintFlow(); + if (flow) { + delete flow; + } + setTaintFlow(taint); + } + + bool isTainted() const { + return !!getTaintFlow(); + } + + inline TaintFlow* getTaintFlow() const { + TaintFlow* n = maybePtrFromReservedSlot(TAINT_SLOT); + return n; + } + + void setPrimitiveValue(JS::Value value) { setFixedSlot(PRIMITIVE_VALUE_SLOT, value);} + +private: static JSObject* createPrototype(JSContext* cx, JSProtoKey key); inline void setPrimitiveValue(double d) { setFixedSlot(PRIMITIVE_VALUE_SLOT, NumberValue(d)); } + + inline void setTaintFlow(const TaintFlow& flow) { + setReservedSlot(TAINT_SLOT, PrivateValue(new TaintFlow(flow))); + } + }; } // namespace js diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 1ca5d460bda37..f82536870fcfd 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -22,6 +22,7 @@ #include "jsfriendapi.h" #include "jsmath.h" #include "jsnum.h" +#include "jstaint.h" #include "selfhosted.out.h" #include "builtin/Array.h" @@ -1875,6 +1876,24 @@ taint_copyString(JSContext* cx, unsigned argc, Value* vp) return true; } +// TaintFox: Returns a the value of a tainted number object, if the input is a +// tainted number object. Otherwise returns the input value without changes. +static bool taint_getNumberObjectValueIfTainted(JSContext* cx, unsigned argc, + Value* vp) { + // String, operation, args... + CallArgs args = CallArgsFromVp(argc, vp); + if (JS::isTaintedNumber(args[0])) { + double value; + if (!ToNumber(cx, args[0], &value)) { + return false; + } + args.rval().setNumber(value); + } else { + args.rval().set(args[0]); + } + return true; +} + static bool intrinsic_PromiseResolve(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 2); @@ -2129,6 +2148,7 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_INLINABLE_FN("GetNextSetEntryForIterator", intrinsic_GetNextSetEntryForIterator, 2, 0, IntrinsicGetNextSetEntryForIterator), + JS_FN("GetNumberObjectValueIfTainted", taint_getNumberObjectValueIfTainted, 1, 0), JS_FN("GetOwnPropertyDescriptorToArray", GetOwnPropertyDescriptorToArray, 2, 0), JS_FN("GetStringDataProperty", intrinsic_GetStringDataProperty, 2, 0), diff --git a/js/src/vm/StringType.cpp b/js/src/vm/StringType.cpp index 0af4ade2f0c2d..c22ceb2d3b22b 100644 --- a/js/src/vm/StringType.cpp +++ b/js/src/vm/StringType.cpp @@ -1031,17 +1031,8 @@ JSString* js::ConcatStrings( typename MaybeRooted::HandleType right, gc::Heap heap) { - TaintOperation op("concat"); - if ((left && right) && (left->taint().hasTaint() || right->taint().hasTaint())) { - op = JS::TaintOperationConcat(cx, "concat", true, left, right); - } - JSString* str = ConcatStringsQuiet(cx, left, right, heap); - if (str && str->taint().hasTaint()) { - str->taint().extend(op); - } - return str; } diff --git a/layout/style/ServoBindings.toml b/layout/style/ServoBindings.toml index d831bccd75e6b..3592dc52c5ab0 100644 --- a/layout/style/ServoBindings.toml +++ b/layout/style/ServoBindings.toml @@ -359,6 +359,12 @@ allowlist-types = [ "NodeSelectorFlags", ] opaque-types = [ + #"TaintNodeIterator", + #"std:set", + #"std:shared_ptr", + #"_NodeHandle", + #"_Node_handle", + "StringTaint", "mozilla::StyleThinArc", # https://github.com/rust-lang/rust-bindgen/issues/1557 "std::pair__PCCP", "std::namespace::atomic___base", "std::atomic__My_base", diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index d5bc3321c6e76..aef3cf60d893a 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4112,6 +4112,86 @@ pref("tainting.source.document.elementsFromPoint", true); pref("tainting.source.element.attribute", true); pref("tainting.source.element.closest", true); +// These are all WebIDL-generated fingerprinting sources +pref("tainting.source.AudioContext.baseLatency", true); +pref("tainting.source.AudioContext.outputLatency", true); +pref("tainting.source.AudioDestinationNode.maxChannelCount", true); +pref("tainting.source.AudioNode.numberOfInputs", true); +pref("tainting.source.AudioNode.numberOfOutputs", true); +pref("tainting.source.AudioNode.channelCount", true); +pref("tainting.source.BaseAudioContext.sampleRate", true); +pref("tainting.source.BaseAudioContext.currentTime", true); +pref("tainting.source.HTMLCanvasElement.toDataURL", true); +pref("tainting.source.HTMLElement.offsetWidth", true); +pref("tainting.source.HTMLElement.offsetHeight", true); +pref("tainting.source.History.length", true); +pref("tainting.source.MediaDeviceInfo.deviceId", true); +pref("tainting.source.MediaDeviceInfo.label", true); +pref("tainting.source.MediaDeviceInfo.groupId", true); +pref("tainting.source.MimeType.type", true); +pref("tainting.source.MimeType.description", true); +pref("tainting.source.MimeType.suffixes", true); +pref("tainting.source.Navigator.doNotTrack", true); +pref("tainting.source.Navigator.maxTouchPoints", true); +pref("tainting.source.Navigator.oscpu", true); +pref("tainting.source.Navigator.vendor", true); +pref("tainting.source.Navigator.vendorSub", true); +pref("tainting.source.Navigator.productSub", true); +pref("tainting.source.Navigator.buildID", true); +pref("tainting.source.Navigator.hardwareConcurrency", true); +pref("tainting.source.Navigator.appCodeName", true); +pref("tainting.source.Navigator.appName", true); +pref("tainting.source.Navigator.appVersion", true); +pref("tainting.source.Navigator.platform", true); +pref("tainting.source.Navigator.userAgent", true); +pref("tainting.source.Navigator.language", true); +pref("tainting.source.Navigator.languages", true); +pref("tainting.source.Plugin.description", true); +pref("tainting.source.Plugin.filename", true); +pref("tainting.source.Plugin.name", true); +pref("tainting.source.Screen.availWidth", true); +pref("tainting.source.Screen.availHeight", true); +pref("tainting.source.Screen.width", true); +pref("tainting.source.Screen.height", true); +pref("tainting.source.Screen.colorDepth", true); +pref("tainting.source.Screen.pixelDepth", true); +pref("tainting.source.Screen.availTop", true); +pref("tainting.source.Screen.availLeft", true); +pref("tainting.source.VisualViewport.offsetLeft", true); +pref("tainting.source.VisualViewport.offsetTop", true); +pref("tainting.source.VisualViewport.pageLeft", true); +pref("tainting.source.VisualViewport.pageTop", true); +pref("tainting.source.VisualViewport.width", true); +pref("tainting.source.VisualViewport.height", true); +pref("tainting.source.VisualViewport.scale", true); +pref("tainting.source.WebGL2RenderingContext.getParameter", true); +pref("tainting.source.WebGLRenderingContext.getParameter", true); +pref("tainting.source.WebGLShaderPrecisionFormat.rangeMin", true); +pref("tainting.source.WebGLShaderPrecisionFormat.rangeMax", true); +pref("tainting.source.WebGLShaderPrecisionFormat.precision", true); +pref("tainting.source.WorkerNavigator.hardwareConcurrency", true); +pref("tainting.source.WorkerNavigator.appCodeName", true); +pref("tainting.source.WorkerNavigator.appName", true); +pref("tainting.source.WorkerNavigator.appVersion", true); +pref("tainting.source.WorkerNavigator.platform", true); +pref("tainting.source.WorkerNavigator.userAgent", true); +pref("tainting.source.WorkerNavigator.product", true); +pref("tainting.source.WorkerNavigator.language", true); +pref("tainting.source.WorkerNavigator.languages", true); +pref("tainting.source.Window.innerWidth", true); +pref("tainting.source.Window.innerHeight", true); +pref("tainting.source.Window.scrollX", true); +pref("tainting.source.Window.pageXOffset", true); +pref("tainting.source.Window.scrollY", true); +pref("tainting.source.Window.pageYOffset", true); +pref("tainting.source.Window.screenLeft", true); +pref("tainting.source.Window.screenTop", true); +pref("tainting.source.Window.screenX", true); +pref("tainting.source.Window.screenY", true); +pref("tainting.source.Window.outerWidth", true); +pref("tainting.source.Window.outerHeight", true); +pref("tainting.source.Window.devicePixelRatio", true); + // Sinks pref("tainting.sink.element.after", true); pref("tainting.sink.element.before", true); diff --git a/taint/README.md b/taint/README.md index ad65c4ae7b64d..a610aa8df1410 100644 --- a/taint/README.md +++ b/taint/README.md @@ -83,6 +83,9 @@ going to [](about:config) and looking for the ```dom.storage.next_gen``` option. TODO: intercept the local storage call and add the taint information to storage as well (e.g. via JSON?) +#### WebIDL Sources +It is now possible to add taint sources to WebIDL getters by adding the TaintSource attribute. +See dom/webidl/Screen.webidl for an example. ### Sinks diff --git a/taint/Taint.cpp b/taint/Taint.cpp index 0868b9415af1c..9c4b08d01e323 100644 --- a/taint/Taint.cpp +++ b/taint/Taint.cpp @@ -142,8 +142,59 @@ void TaintOperation::dump(const TaintOperation& op) { void TaintOperation::dump(const TaintOperation& op) {} #endif -TaintNode::TaintNode(TaintNode* parent, const TaintOperation& operation) - : parent_(parent), refcount_(1), operation_(operation) +bool TaintNodeSourceCollector::visit(TaintNode& node) { + n++; + if (visited_.count(&node)) { + return false; + } + if (node.operation().isSource()) { + sources_.insert(node.operation()); + } + visited_.insert(&node); + return true; +} + +bool TaintNodeSourceCollector::visit(BinaryTaintNode& node) { + n++; + if (visited_.count(&node)) { + return false; + } + if (node.operation().isSource()) { + sources_.insert(node.operation()); + } + visited_.insert(&node); + return true; +} + +TaintNodeBase::TaintNodeBase(const TaintOperation& operation) + : refcount_(1), operation_(operation) +{} + +TaintNodeBase::TaintNodeBase(TaintOperation&& operation) + : refcount_(1), operation_(std::move(operation)) +{} + +void TaintNodeBase::addref() +{ + if (refcount_ == 0xffffffff) { + MOZ_CRASH("TaintNode refcount overflow"); + } + + ++refcount_; +} + +void TaintNodeBase::release() +{ + MOZ_ASSERT(refcount_ > 0); + + --refcount_; + if (refcount_ == 0) { + delete this; + } +} + +TaintNode::TaintNode(TaintNodeBase* parent, const TaintOperation& operation) + : TaintNodeBase(operation), parent_(parent) { MOZ_COUNT_CTOR(TaintNode); if (parent_) { @@ -151,8 +202,8 @@ TaintNode::TaintNode(TaintNode* parent, const TaintOperation& operation) } } -TaintNode::TaintNode(TaintNode* parent, TaintOperation&& operation) noexcept - : parent_(parent), refcount_(1), operation_(std::move(operation)) +TaintNode::TaintNode(TaintNodeBase* parent, TaintOperation&& operation) noexcept + : TaintNodeBase(operation), parent_(parent) { MOZ_COUNT_CTOR(TaintNode); if (parent_) { @@ -161,46 +212,84 @@ TaintNode::TaintNode(TaintNode* parent, TaintOperation&& operation) noexcept } TaintNode::TaintNode(const TaintOperation& operation) - : parent_(nullptr), refcount_(1), operation_(operation) + : TaintNodeBase(operation), parent_(nullptr) { MOZ_COUNT_CTOR(TaintNode); } TaintNode::TaintNode(TaintOperation&& operation) noexcept - : parent_(nullptr), refcount_(1), operation_(std::move(operation)) + : TaintNodeBase(operation), parent_(nullptr) { MOZ_COUNT_CTOR(TaintNode); } -void TaintNode::addref() +TaintNode::~TaintNode() { - if (refcount_ == 0xffffffff) { - MOZ_CRASH("TaintNode refcount overflow"); + MOZ_COUNT_DTOR(TaintNode); + if (parent_) { + parent_->release(); } +} - ++refcount_; +void TaintNode::accept(TaintNodeVisitor& visitor) +{ + if (visitor.visit(*this)) { + if (parent_) { + parent_->accept(visitor); + } + } } -void TaintNode::release() + +BinaryTaintNode::BinaryTaintNode(TaintNodeBase* p1, TaintNodeBase* p2, const TaintOperation& operation) + : TaintNodeBase(operation), parent1_(p1), parent2_(p2) { - MOZ_ASSERT(refcount_ > 0); + MOZ_COUNT_CTOR(BinaryTaintNode); + if (parent1_) { + parent1_->addref(); + } + if (parent2_) { + parent2_->addref(); + } +} - --refcount_; - if (refcount_ == 0) { - delete this; +BinaryTaintNode::BinaryTaintNode(TaintNodeBase* p1, TaintNodeBase* p2, TaintOperation&& operation) noexcept + : TaintNodeBase(operation), parent1_(p1), parent2_(p2) +{ + MOZ_COUNT_CTOR(BinaryTaintNode); + if (parent1_) { + parent1_->addref(); + } + if (parent2_) { + parent2_->addref(); } } -TaintNode::~TaintNode() +BinaryTaintNode::~BinaryTaintNode() { - MOZ_COUNT_DTOR(TaintNode); - if (parent_) { - parent_->release(); + MOZ_COUNT_DTOR(BinaryTaintNode); + if (parent1_) { + parent1_->release(); + } + if (parent2_) { + parent2_->release(); + } +} + +void BinaryTaintNode::accept(TaintNodeVisitor& visitor) +{ + if (visitor.visit(*this)) { + if (parent1_) { + parent1_->accept(visitor); + } + if (parent2_) { + parent2_->accept(visitor); + } } } -TaintFlow::Iterator::Iterator(TaintNode* head) : current_(head) { } +TaintFlow::Iterator::Iterator(TaintNodeBase* head) : current_(head) { } TaintFlow::Iterator::Iterator() : current_(nullptr) { } @@ -212,7 +301,7 @@ TaintFlow::Iterator& TaintFlow::Iterator::operator++() return *this; } -TaintNode& TaintFlow::Iterator::operator*() const +TaintNodeBase& TaintFlow::Iterator::operator*() const { return *current_; } @@ -233,7 +322,7 @@ TaintFlow::TaintFlow() MOZ_COUNT_CTOR(TaintFlow); } -TaintFlow::TaintFlow(TaintNode* head) +TaintFlow::TaintFlow(TaintNodeBase* head) : head_(head) { MOZ_COUNT_CTOR(TaintFlow); @@ -307,7 +396,7 @@ const TaintFlow& TaintFlow::getEmptyTaintFlow() { const TaintOperation& TaintFlow::source() const { - TaintNode* source = head_; + TaintNodeBase* source = head_; while (source->parent() != nullptr) { source = source->parent(); } @@ -357,20 +446,37 @@ TaintFlow TaintFlow::extend(const TaintFlow& flow, const TaintOperation& operati return TaintFlow(new TaintNode(flow.head_, operation)); } +TaintFlow TaintFlow::append(const TaintFlow& first, const TaintFlow& second, const TaintOperation& operation) +{ + if (first.head_ == second.head_) { + return TaintFlow(new TaintNode(first.head_, operation)); + } + return TaintFlow(new BinaryTaintNode(first.head_, second.head_, operation)); +} + TaintFlow TaintFlow::append(const TaintFlow& first, const TaintFlow& second) { TaintFlow outFlow(first); - std::stack q; - for (const TaintNode& node : second) { + std::stack q; + for (const TaintNodeBase& node : second) { q.push(&node); } for (; !q.empty(); q.pop()) { - const TaintNode* node = q.top(); + const TaintNodeBase* node = q.top(); outFlow.extend(node->operation()); } return outFlow; } +std::unordered_set TaintFlow::getSources() const { + if (!head_) { + return std::unordered_set(); + } + TaintNodeSourceCollector collector; + head_->accept(collector); + return collector.getSources(); +} + TaintRange::TaintRange() : begin_(0), end_(0), flow_() { @@ -1335,4 +1441,4 @@ void TaintDebug(std::string_view message, << message << std::endl; } -#endif \ No newline at end of file +#endif diff --git a/taint/Taint.h b/taint/Taint.h index 5d17114bb8f72..f36ccc40314c8 100644 --- a/taint/Taint.h +++ b/taint/Taint.h @@ -16,15 +16,24 @@ #include #include #include +#include #include #include +#define TAINT_DEBUG + // It appears that source_location is not supported as standard and // breaks windows builds. Keep it behind a debug macro for now. #ifdef TAINT_DEBUG #include #endif +// Forward Declarations +class TaintNode; +class BinaryTaintNode; +class TaintFlow; +class TaintNodeBase; + /* * How to taint: * @@ -48,8 +57,12 @@ * as well as a set of operations that were performed on * the original data. * - * TaintNode A node in a taint flow. These represent either a taint - * source or an operation. + * TaintNodeBase A node in a taint flow. These represent either a taint + * source or an operation. This is the base class. + * + * TaintNode A node in a taint flow with a single parent. + * + * BinaryTaintNode A node in a taint flow with two parents. * * TaintRange A range of tainted bytes within a string that all share * the same taint flow. @@ -96,6 +109,15 @@ class TaintLocation const TaintMd5& scriptHash() const { return scriptHash_; } const std::u16string& function() const { return function_; } + bool operator==(const TaintLocation& other) const { + return (filename_ == other.filename_) && + (line_ == other.line_) && + (pos_ == other.pos_) && + (scriptStartLine_ == other.scriptStartLine_) && + (scriptHash_ == other.scriptHash_) && + (function_ == other.function_); + }; + private: std::u16string filename_; uint32_t line_; @@ -154,7 +176,15 @@ class TaintOperation void set_native() { is_native_ = 1; } static void dump(const TaintOperation& op); - + + bool operator==(const TaintOperation& other) const { + return (name_ == other.name_) && + (arguments_ == other.arguments_) && + (source_ == other.source_) && + (is_native_ == other.is_native_) && + (location_ == other.location_); + }; + private: // The operation name is owned by this instance. It will be copied from the // argument string during construction. @@ -172,6 +202,94 @@ class TaintOperation TaintLocation location_; }; +// The following are all hash implementations required to use TaintOperation in a std::hash +namespace std { + template <> + struct hash { + size_t operator()(const TaintMd5& digest) const { + size_t h = 0; + size_t i = 0; + for (const auto& byte : digest) { + if (i == 0) { + h = hash()(byte); + } + h = h ^ (hash()(byte) << i); + i++; + } + return h; + } + }; + template <> + struct hash { + size_t operator()(const TaintLocation& p) const { + size_t h1 = 0; + h1 = hash()(p.filename()); + h1 = h1 ^ (hash()(p.line()) << 1); + h1 = h1 ^ (hash()(p.line()) << 2); + h1 = h1 ^ (hash()(p.pos()) << 3); + h1 = h1 ^ (hash()(p.scriptStartLine()) << 4); + h1 = h1 ^ (hash()(p.scriptHash()) << 5); + h1 = h1 ^ (hash()(p.function()) << 6); + return h1; + } + }; + + template <> + struct hash> { + size_t operator()(const std::vector& v) const { + size_t h = 0; + size_t i = 0; + for (const auto& s : v) { + if (i == 0) { + h = hash()(s); + } + h = h ^ (hash()(s) << i); + i++; + } + return h; + } + }; + + template <> + struct hash { + size_t operator()(const TaintOperation& p) const { + size_t h1 = 0; + h1 = hash()(p.name()); + h1 = h1 ^ (hash>()(p.arguments()) << 1); + h1 = h1 ^ (hash()(p.isSource()) << 2); + h1 = h1 ^ (hash()(p.is_native()) << 3); + h1 = h1 ^ (hash()(p.location()) << 4); + return h1; + } + }; +} + +// Base class to visit all nodes of a TaintFlow +class TaintNodeVisitor +{ + public: + virtual bool visit(TaintNode& node) = 0; + virtual bool visit(BinaryTaintNode& node) = 0; +}; + +// Concrete implementation to collect unique sources in a TaintFlow +class TaintNodeSourceCollector : public TaintNodeVisitor +{ + public: + TaintNodeSourceCollector() : n(0), sources_(), visited_() {}; + + virtual bool visit(TaintNode& node); + virtual bool visit(BinaryTaintNode& node); + + // Use an unordered set to prevent duplication of same sources + std::unordered_set& getSources() { return sources_; } + int n; + private: + std::unordered_set sources_; + std::unordered_set visited_; +}; + + /* * The nodes of the taint flow graph. @@ -186,21 +304,13 @@ class TaintOperation * integrated into other engines) we are implementing our own reference counting * here. */ -class TaintNode +class TaintNodeBase { public: - // Constructing a taint node sets the initial reference count to 1. - // Constructs an intermediate node. - TaintNode(TaintNode* parent, const TaintOperation& operation); - // Constructs a root node. - TaintNode(const TaintOperation& operation); + TaintNodeBase(const TaintOperation& operation); + + TaintNodeBase(TaintOperation&& operation); - // Constructing a taint node sets the initial reference count to 1. - // Constructs an intermediate node. - TaintNode(TaintNode* parent, TaintOperation&& operation) noexcept; - // Constructs a root node. - TaintNode(TaintOperation&& operation) noexcept; - // Increments the reference count of this object by one. void addref(); @@ -208,31 +318,98 @@ class TaintNode // count drops to zero then this instance will be destroyed. void release(); - // Returns the parent node of this node or nullptr if this is a root node. - TaintNode* parent() { return parent_; } + virtual TaintNodeBase* parent() = 0; + + virtual void accept(TaintNodeVisitor& visitor) = 0; // Returns the operation associated with this taint node. const TaintOperation& operation() const { return operation_; } - private: - // Prevent clients from deleting us. TaintNodes can only be destroyed - // through release(). - ~TaintNode(); + protected: - // A node takes care of correctly addref()ing and release()ing its parent node. - TaintNode* parent_; + virtual ~TaintNodeBase() {}; std::atomic_uint32_t refcount_; // The operation that led to the creation of this node. TaintOperation operation_; +}; + + +// A TaintNode has a single parent, mostly used to model unary operations +// e.g. most string operations +class TaintNode : public TaintNodeBase +{ + public: + // Constructing a taint node sets the initial reference count to 1. + // Constructs an intermediate node. + TaintNode(TaintNodeBase* parent, const TaintOperation& operation); + // Constructs a root node. + TaintNode(const TaintOperation& operation); + + // Constructing a taint node sets the initial reference count to 1. + // Constructs an intermediate node. + TaintNode(TaintNodeBase* parent, TaintOperation&& operation) noexcept; + // Constructs a root node. + TaintNode(TaintOperation&& operation) noexcept; + + // Returns the parent node of this node or nullptr if this is a root node. + TaintNodeBase* parent() { return parent_; } + + virtual void accept(TaintNodeVisitor& visitor); + + private: + // Prevent clients from deleting us. TaintNodes can only be destroyed + // through release(). + ~TaintNode(); + + // A node takes care of correctly addref()ing and release()ing its parent node. + TaintNodeBase* parent_; + // TaintNodes aren't supposed to be copied or assigned to (that's why we // refcount them), so these operations are unavailable. TaintNode(const TaintNode& other) = delete; TaintNode& operator=(const TaintNode& other) = delete; }; + +// A BinaryTaintNode has two parents, common for all numeric operations +class BinaryTaintNode : public TaintNodeBase +{ + public: + // Constructing a taint node sets the initial reference count to 1. + // Constructs an intermediate node. + BinaryTaintNode(TaintNodeBase* p1, TaintNodeBase* p2, const TaintOperation& operation); + + // Constructing a taint node sets the initial reference count to 1. + // Constructs an intermediate node. + BinaryTaintNode(TaintNodeBase* p1, TaintNodeBase* p2, TaintOperation&& operation) noexcept; + + // Always return the first parent + TaintNodeBase* parent() { return parent1_; } + + virtual void accept(TaintNodeVisitor& visitor); + + TaintNodeBase* parent1() { return parent1_; } + TaintNodeBase* parent2() { return parent2_; } + + private: + // Prevent clients from deleting us. TaintNodes can only be destroyed + // through release(). + ~BinaryTaintNode(); + + // A node takes care of correctly addref()ing and release()ing its parent node. + TaintNodeBase* parent1_; + TaintNodeBase* parent2_; + + // TaintNodes aren't supposed to be copied or assigned to (that's why we + // refcount them), so these operations are unavailable. + BinaryTaintNode(const BinaryTaintNode& other) = delete; + BinaryTaintNode& operator=(const BinaryTaintNode& other) = delete; +}; + + /* * Taint flows. * @@ -275,23 +452,27 @@ class TaintFlow private: // Iterate over the nodes in this flow. // - // Note: The iterator does not increment the reference count. Instead, the + // Warning: The iterator does not increment the reference count. Instead, the // caller must ensure that the TaintFlow instance is alive during the // lifetime of any iterator instance. + // + // Note: In the presence of binary nodes, the iterator will only visit the + // first parent in each case. This is to maintain backward compatibility with + // previous versions which only contained single-parent Nodes. class Iterator { public: - Iterator(TaintNode* head); + Iterator(TaintNodeBase* head); Iterator(); Iterator(const Iterator& other); Iterator& operator++(); - TaintNode& operator*() const; + TaintNodeBase& operator*() const; bool operator==(const Iterator& other) const; bool operator!=(const Iterator& other) const; private: - TaintNode* current_; + TaintNodeBase* current_; }; public: @@ -303,7 +484,7 @@ class TaintFlow // *not* increment its refcount. // This is usually what the caller wants: // TaintFlow flow(new TaintNode(...)); - explicit TaintFlow(TaintNode* head); + explicit TaintFlow(TaintNodeBase* head); // Construct a new taint flow from the provided taint source. TaintFlow(const TaintOperation& source); @@ -322,7 +503,7 @@ class TaintFlow TaintFlow& operator=(const TaintFlow& other); // Returns the head of this flow, i.e. the newest node in the path. - TaintNode* head() const { return head_; } + TaintNodeBase* head() const { return head_; } // Returns the source of this taint flow. const TaintOperation& source() const; @@ -353,6 +534,7 @@ class TaintFlow static TaintFlow extend(const TaintFlow& flow, const TaintOperation& operation); // Append two taint flows together + static TaintFlow append(const TaintFlow& first, const TaintFlow& second, const TaintOperation& operation); static TaintFlow append(const TaintFlow& first, const TaintFlow& second); // Two TaintFlows are equal if they point to the same taint node. @@ -366,9 +548,12 @@ class TaintFlow static const TaintFlow& getEmptyTaintFlow(); + // Traverse the entire TaintFlow tree and extract unique sources + std::unordered_set getSources() const; + private: // Last (newest) node of this flow. - TaintNode* head_; + TaintNodeBase* head_; static TaintFlow empty_flow_; }; diff --git a/taint/docs/NumberTainting.md b/taint/docs/NumberTainting.md new file mode 100644 index 0000000000000..b3900c391b247 --- /dev/null +++ b/taint/docs/NumberTainting.md @@ -0,0 +1,29 @@ +# Making Numbers Taint-aware + +There are several approaches to making the number type taint aware, ranging from +changing the JSValue type itself to creating lookup tables for the taint +information. + +Our approach works by extending the NumberObject type (new Number(...)) to +containt the taint information, and then changing various functions (str.charCodeAt(), +arithmetic operations, binary operations, etc.) throughout the engine to produce NumberObjects if the +result of the computation is tainted. +Moreover, features such as typeof were modified to make tainted numbers look +like primitive numbers to the JavaScript code. +This works quite well as it gives us basic JIT support "for free" since the +NumberObject type isn't specifically optimized for, and so fallbacks to the +interpreter happen automatically (unlike with strings, where we have to +force a fallback if we detect a tainted string in the JIT code). + +However, there are still issues with approach. For example, the bitshift +operations are defined to produce a 32 bit integer as result [1], a fact that the Ion +JIT compiler optimizes for. So, given e.g. a sequence of bitshifts and +additions, ((a << 16) + (b << 8) + c), the Ion compiler will likely inline the +additions (instead of calling AddOperation), since it now assumes the result +must be an integer. This assumption is violated by our approch (now the result +is an Object) and so this code breaks. +The easiest way to deal with this for now is to disable the Ion JIT compiler. Fixing +these specific cases would be preferable though. + + +[1] http://www.ecma-international.org/ecma-262/6.0/#sec-left-shift-operator-runtime-semantics-evaluation diff --git a/xpcom/string/nsReadableUtils.cpp b/xpcom/string/nsReadableUtils.cpp index 5e33f586bad94..d80a9a8681c1d 100644 --- a/xpcom/string/nsReadableUtils.cpp +++ b/xpcom/string/nsReadableUtils.cpp @@ -241,7 +241,6 @@ void ToUpperCase(const nsACString& aSource, nsACString& aDest) { // TaintFox: propagate taint into aDest. aDest.AssignTaint(aSource.Taint()); - aDest.Taint().extend(TaintOperation("ToUpperCase", true)); } void ToLowerCase(nsACString& aCString) { @@ -274,7 +273,6 @@ void ToLowerCase(const nsACString& aSource, nsACString& aDest) { // TaintFox: propagate taint into aDest. aDest.AssignTaint(aSource.Taint()); - aDest.Taint().extend(TaintOperation("ToLowerCase", true)); } void ParseString(const nsACString& aSource, char aDelimiter,