Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report taint sink non string #8

Open
wants to merge 10 commits into
base: primitaint-merge
Choose a base branch
from
2 changes: 2 additions & 0 deletions docshell/base/BrowsingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
#include "nsQueryObject.h"
#include "nsSandboxFlags.h"
#include "nsScriptError.h"
#include "nsTaintingUtils.h"
#include "nsThreadUtils.h"
#include "xpcprivate.h"

Expand Down Expand Up @@ -2394,6 +2395,7 @@ void BrowsingContext::PostMessageMoz(JSContext* aCx,
const Sequence<JSObject*>& aTransfer,
nsIPrincipal& aSubjectPrincipal,
ErrorResult& aError) {
ReportTaintSink(aCx, aMessage, "BrowsingContext.PostMessage");
if (mIsDiscarded) {
return;
}
Expand Down
2 changes: 2 additions & 0 deletions dom/messagechannel/MessagePort.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "mozilla/Unused.h"
#include "nsContentUtils.h"
#include "nsPresContext.h"
#include "nsTaintingUtils.h"

#ifdef XP_WIN
# undef PostMessage
Expand Down Expand Up @@ -284,6 +285,7 @@ JSObject* MessagePort::WrapObject(JSContext* aCx,
void MessagePort::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
const Sequence<JSObject*>& aTransferable,
ErrorResult& aRv) {
ReportTaintSink(aCx, aMessage, "MessagePort.PostMessage");
// We *must* clone the data here, or the JS::Value could be modified
// by script

Expand Down
2 changes: 2 additions & 0 deletions dom/workers/Worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "nsDebug.h"
#include "mozilla/dom/WorkerStatus.h"
#include "mozilla/RefPtr.h"
#include "nsTaintingUtils.h"

#ifdef XP_WIN
# undef PostMessage
Expand Down Expand Up @@ -86,6 +87,7 @@ JSObject* Worker::WrapObject(JSContext* aCx,
void Worker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
const Sequence<JSObject*>& aTransferable,
ErrorResult& aRv) {
ReportTaintSink(aCx, aMessage, "Worker.PostMessage");
NS_ASSERT_OWNINGTHREAD(Worker);

if (!mWorkerPrivate || mWorkerPrivate->ParentStatusProtected() > Running) {
Expand Down
205 changes: 115 additions & 90 deletions js/src/jsapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4927,114 +4927,139 @@ JS_ReportTaintSink(JSContext* cx, JS::HandleString str, const char* sink)
{
RootedValue arg(cx);
arg.setNull();
JS_ReportTaintSink(cx, str, sink, arg);
JS::RootedValue handleVal(cx, JS::StringValue(str));
JS_ReportTaintSink(cx, handleVal, sink, arg);
}

JS_PUBLIC_API void
JS_ReportTaintSink(JSContext* cx, JS::HandleValue value, const char* sink, JS::HandleValue arg)
{
if (value.isString()) {
JSString *str = value.toString();
if (str) {
JS::RootedString strobj(cx, str);
JS_ReportTaintSink(cx, strobj, sink, arg);
if (!str)
return;

JS::RootedString strobj(cx, str);
if(!strobj->isTainted())
return;

// Extend the taint flow to include the sink function
strobj->taint().extend(TaintOperationFromContext(cx, sink, true, arg, true));

JS_ReportTaintSink(cx, sink, arg, strobj->taint().begin()->flow(), value, false);
}
else if(isTaintedNumber(value)){
NumberObject& number = value.toObject().as<NumberObject>();

// Extend the taint flow to include the sink function
number.taint().extend(TaintOperationFromContext(cx, sink, true, arg, true));

JS_ReportTaintSink(cx, sink, arg, number.taint(), value, true);

}
else if(value.isObject()){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you want to do here, but this could be fairly spammy. Is this missing a check for taintedness?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree that it could be pretty spammy going over an object recursively to see if it is tainted. However, I don't see a better way of doing it. The JSObject does not hold a flag to say if it is tainted or not. So, to know if a value inside this object is tainted, we have to go over it recursively. On the bright side, this piece of code will not be called often. Most sinks like XML Http request will automatically convert JSObjects to nsACString before calling the report taint sinks.

Under the hood, XML Http request will take an object and do the equivalent of JSON.Stringify before calling the send function. So from a report taint sink perspective, it will see this as a string not an object and the the recursive code will not be called.

This change is to support the new sink that I have added for MessagePort.postMessage. This is one of the only sinks that sends the JSValue itself instead of the nsACString. For this case, we have to look over the object and check to see if it is tainted or not.

That being I am open to suggestions on how to make this code better. Maybe add a flag to JSObject to keep track if it is tainted or not?

JS::Rooted<JS::IdVector> props(cx, JS::IdVector(cx));
JS::RootedObject rootedObj(cx, &value.toObject());
JS_Enumerate(cx, rootedObj, &props);
for (size_t i = 0; i < props.length(); i++) {
JS::RootedId id(cx, props[i]);
JS::RootedValue val(cx);
JS_GetPropertyById(cx, rootedObj, id, &val);
JS_ReportTaintSink(cx, val, sink, arg);
}
}
}

JS_PUBLIC_API void
JS_ReportTaintSink(JSContext* cx, JS::HandleString str, const char* sink, JS::HandleValue arg)
JS_ReportTaintSink(JSContext* cx, const char* sink, JS::HandleValue arg, TaintFlow flow, JS::HandleValue value, bool isNumber)
{
const unsigned TAINT_REPORT_FUNCTION_SLOT = 5;

if (!str->isTainted()) {
return;
}

MOZ_ASSERT(!cx->isExceptionPending());

// Print a message to stdout. Also include the current JS backtrace.
auto& firstRange = *str->taint().begin();

std::cerr << "!!! Tainted flow into " << sink << " from " << firstRange.flow().source().name() << " !!!" << std::endl;
// DumpBacktrace(cx);

// Report a warning to show up on the web console
JS_ReportWarningUTF8(cx, "Tainted flow from %s into %s!", firstRange.flow().source().name(), sink);

// Extend the taint flow to include the sink function
str->taint().extend(TaintOperationFromContext(cx, sink, true, arg, true));

// Trigger a custom event that can be caught by an extension.
// To simplify things, this part is implemented in JavaScript. Since we don't want to recompile
// this code everytime we detect a tainted flow, we store the compiled function into a reserved
// slot of the current global object.
RootedFunction report(cx);

JSObject* global = cx->global();

RootedValue slot(cx, JS::GetReservedSlot(global, TAINT_REPORT_FUNCTION_SLOT));
if (slot.isUndefined()) {
// Need to compile.
const char* argnames[3] = {"str", "sink", "stack"};
const char* funbody =
"if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n"
" var t = window;\n"
" if (location.protocol == 'javascript:' || location.protocol == 'data:' || location.protocol == 'about:') {\n"
" t = parent.window;\n"
" }\n"
" var pl;\n"
" try {\n"
" pl = parent.location.href;\n"
" } catch (e) {\n"
" pl = 'different origin';\n"
" }\n"
" var e = document.createEvent('CustomEvent');\n"
" e.initCustomEvent('__taintreport', true, false, {\n"
" subframe: t !== window,\n"
" loc: location.href,\n"
" parentloc: pl,\n"
" referrer: document.referrer,\n"
" str: str,\n"
" sink: sink,\n"
" stack: stack\n"
" });\n"
" t.dispatchEvent(e);\n"
"}";
CompileOptions options(cx);
options.setFile("taint_reporting.js");

RootedObjectVector emptyScopeChain(cx);
report = CompileFunctionUtf8(cx, emptyScopeChain,
options, "ReportTaintSink", 3,
argnames, funbody, strlen(funbody));
MOZ_ASSERT(report);

// Store the compiled function into the current global object.
JS_SetReservedSlot(global, TAINT_REPORT_FUNCTION_SLOT, ObjectValue(*report));
} else {
report = JS_ValueToFunction(cx, slot);
}
MOZ_ASSERT(!cx->isExceptionPending());

// Print a message to stdout. Also include the current JS backtrace.
std::cerr << "!!! Tainted flow into " << sink << " from " << flow.source().name() << " !!!" << std::endl;
// DumpBacktrace(cx);

// Report a warning to show up on the web console
JS_ReportWarningUTF8(cx, "Tainted flow from %s into %s!", flow.source().name(), sink);

// Trigger a custom event that can be caught by an extension.
// To simplify things, this part is implemented in JavaScript. Since we don't want to recompile
// this code everytime we detect a tainted flow, we store the compiled function into a reserved
// slot of the current global object.
RootedFunction report(cx);

JSObject* global = cx->global();

RootedValue slot(cx, JS::GetReservedSlot(global, TAINT_REPORT_FUNCTION_SLOT));
if (slot.isUndefined()) {
// Need to compile.
const char* argnames[3] = {"str", "sink", "stack"};
const char* funbody =
"if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n"
" var t = window;\n"
" if (location.protocol == 'javascript:' || location.protocol == 'data:' || location.protocol == 'about:') {\n"
" t = parent.window;\n"
" }\n"
" var pl;\n"
" try {\n"
" pl = parent.location.href;\n"
" } catch (e) {\n"
" pl = 'different origin';\n"
" }\n"
" var e = document.createEvent('CustomEvent');\n"
" e.initCustomEvent('__taintreport', true, false, {\n"
" subframe: t !== window,\n"
" loc: location.href,\n"
" parentloc: pl,\n"
" referrer: document.referrer,\n"
" str: str,\n"
" sink: sink,\n"
" stack: stack\n"
" });\n"
" t.dispatchEvent(e);\n"
"}";
CompileOptions options(cx);
options.setFile("taint_reporting.js");

RootedObjectVector emptyScopeChain(cx);
report = CompileFunctionUtf8(cx, emptyScopeChain,
options, "ReportTaintSink", 3,
argnames, funbody, strlen(funbody));
MOZ_ASSERT(report);

// Store the compiled function into the current global object.
JS_SetReservedSlot(global, TAINT_REPORT_FUNCTION_SLOT, ObjectValue(*report));
} else {
report = JS_ValueToFunction(cx, slot);
}

RootedObject stack(cx);
if (!JS::CaptureCurrentStack(cx, &stack,
JS::StackCapture(JS::AllFrames()))) {
JS_ReportErrorUTF8(cx, "Invalid stack object in CaptureCurrentStack!");
return;
}
RootedObject stack(cx);
if (!JS::CaptureCurrentStack(cx, &stack,
JS::StackCapture(JS::AllFrames()))) {
JS_ReportErrorUTF8(cx, "Invalid stack object in CaptureCurrentStack!");
return;
}

JS::RootedValueArray<3> arguments(cx);
arguments[0].setString(str);
arguments[1].setString(NewStringCopyZ<CanGC>(cx, sink));
if (stack) {
arguments[2].setObject(*stack);
} else {
arguments[2].setUndefined();
}
JS::RootedValueArray<3> arguments(cx);
if(isNumber){
arguments[0].setNumber(value.toObject().as<NumberObject>().unbox());
}
else{
JS::RootedString strobj(cx, value.toString());
arguments[0].setString(strobj);
}
arguments[1].setString(NewStringCopyZ<CanGC>(cx, sink));
if (stack) {
arguments[2].setObject(*stack);
} else {
arguments[2].setUndefined();
}

RootedValue rval(cx);
JS_CallFunction(cx, nullptr, report, arguments, &rval);
MOZ_ASSERT(!cx->isExceptionPending());
RootedValue rval(cx);
JS_CallFunction(cx, nullptr, report, arguments, &rval);
MOZ_ASSERT(!cx->isExceptionPending());
}

JS_PUBLIC_API bool JS::FinishIncrementalEncoding(JSContext* cx,
Expand Down
7 changes: 3 additions & 4 deletions js/src/jsapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -1032,17 +1032,16 @@ JS_PUBLIC_API void
JS_MarkTaintSource(JSContext* cx, JSString* str, const TaintOperation& operation);

// TaintFox: Report tainted flows into a sink.
//
// This will print to stdout and trigger a custom JavaScript event on the current page.
extern JS_PUBLIC_API void
JS_ReportTaintSink(JSContext* cx, JS::HandleString str, const char* sink, JS::HandleValue args);

extern JS_PUBLIC_API void
JS_ReportTaintSink(JSContext* cx, JS::HandleValue value, const char* sink, JS::HandleValue args);

extern JS_PUBLIC_API void
JS_ReportTaintSink(JSContext* cx, JS::HandleString str, const char* sink);

extern JS_PUBLIC_API void
JS_ReportTaintSink(JSContext* cx, const char* sink, JS::HandleValue arg, TaintFlow flow, JS::HandleValue value, bool isNumber);

extern JS_PUBLIC_API void
JS_ReportTaintSink(JSContext* cx, JS::HandleValue val, const char* sink);

Expand Down
1 change: 1 addition & 0 deletions modules/libpref/init/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -4158,6 +4158,7 @@ pref("tainting.sink.location.protocol", true);
pref("tainting.sink.location.replace", true);
pref("tainting.sink.location.search", true);
pref("tainting.sink.media.src", true);
pref("tainting.sink.MessagePort.PostMessage", true);
pref("tainting.sink.navigator.sendBeacon(body)", true);
pref("tainting.sink.navigator.sendBeacon(url)", true);
pref("tainting.sink.object.data", true);
Expand Down
2 changes: 2 additions & 0 deletions taint/test/mochitest/mochitest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ support-files =
[test_websocket.html]
[test_push.html]
[test_dom.html]
[test_message_port_sink.html]
[test_non_string_sinks.html]
scheme = https
[test_url_object.html]
[test_message_event.html]
77 changes: 77 additions & 0 deletions taint/test/mochitest/test_message_port_sink.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test HTML Message Port Taint Sink</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
<script>

let string_content = "hello";
let sink_name = "MessagePort.PostMessage";
let number_of_tainted_flows = 1;

let i = 0;

SimpleTest.waitForExplicitFinish();
addEventListener("__taintreport", (report) => {
is(report.detail.str, string_content, "Check sink string content");

let flow = report.detail.str.taint[0].flow;
is(flow[0].operation, sink_name);

i += 1;
if (i >= number_of_tainted_flows) {
SimpleTest.finish();
}
}, false);

let taint_string = String.tainted(string_content);

// Worker script as a Blob
const workerScript = `
self.onmessage = (event) => {
if (event.data === 'initialize') {
// Retrieve the port from the message
const port = event.ports[0];

// Listen for messages on the port
port.onmessage = (event) => {
console.log(JSON.stringify(event));
port.postMessage("Received the event");
};

// Send an initial message back to the main page
port.postMessage('Worker initialized and ready.');
}
};
`;
if (window.Worker) {
const blob = new Blob([workerScript], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);

const worker = new Worker(workerUrl);

const channel = new MessageChannel();

// Send one of the ports to the worker
worker.postMessage('initialize', [channel.port1]);

// Listen for messages from the worker
channel.port2.onmessage = (event) => {
console.log('Message received from worker:', event.data);
};

channel.port2.postMessage(taint_string);
} else {
console.error('No support for workers');
}
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<p id="test"></p>
<button id="btn"></button>
</body>
</html>
Loading