diff options
-rw-r--r-- | jstests/noPassthrough/js_protection.js | 67 | ||||
-rw-r--r-- | jstests/noPassthrough/js_protection_roundtrip.js | 48 | ||||
-rw-r--r-- | src/mongo/scripting/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/scripting/engine.h | 3 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/code.cpp | 96 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/code.h | 55 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/engine.cpp | 9 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/engine.h | 3 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/implscope.cpp | 6 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/implscope.h | 10 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/internedstring.defs | 2 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/valuereader.cpp | 32 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/valuewriter.cpp | 21 | ||||
-rw-r--r-- | src/mongo/shell/db.js | 7 | ||||
-rw-r--r-- | src/mongo/shell/dbshell.cpp | 1 | ||||
-rw-r--r-- | src/mongo/shell/shell_options.cpp | 8 | ||||
-rw-r--r-- | src/mongo/shell/shell_options.h | 1 |
17 files changed, 364 insertions, 6 deletions
diff --git a/jstests/noPassthrough/js_protection.js b/jstests/noPassthrough/js_protection.js new file mode 100644 index 00000000000..d500a87fda7 --- /dev/null +++ b/jstests/noPassthrough/js_protection.js @@ -0,0 +1,67 @@ +/** + * Positive tests for the behavior of --enableJavaScriptProtection (the flag). + * + * Ensure that: + * 1. Simple functions stored in documents are not automatically marshalled + * when the flag is on in the shell. + * 2. $where is unable to use stored functions when the flag is set on the + * server. + * 3. db.loadServerScripts performs as expected even with the flag is set in + * the shell. + */ + +(function() { +"use strict"; + +var testServer = MongoRunner.runMongod({setParameter: 'javascriptProtection=true'}); +var db = testServer.getDB("test"); +var t = db.foo; +var funcToStore = function(x) { return x + 1; }; + +function assertMongoClientCorrect() { + var mongo = runMongoProgram("mongo", + "--port", testServer.port, + "--enableJavaScriptProtection", + "--eval", + // stored functions in objects + "var x = db.foo.findOne({'_id' : 0});" + + "assert.neq(typeof x.foo, 'function');" + + // retain db.loadServerScripts() functionality + "db.loadServerScripts();" + + "assert.eq(stored_func(4), 5);" + + + "print(\"completed gracefully\");" + ); + + var mongoOutput = rawMongoProgramOutput(); + assert(!mongoOutput.match(/assert failed/)); + assert(mongoOutput.match(/completed gracefully/)); +} + +function assertNoStoredWhere() { + t.insertOne({name: 'testdoc', val : 0, y : 0}); + t.update( { $where : "stored_func(this.val) == 1" }, + { $set : { y : 100 } } , false , true ); + + var x = t.findOne({name: 'testdoc'}); + assert.eq(x.y, 0); + + t.update( { $where : function() { return this.val == 0;} } , + { $set : { y : 100 } } , false , true ); + + x = t.findOne({name: 'testdoc'}); + assert.eq(x.y, 100); +} + +/** + * ACTUAL TEST + */ + +db.system.js.save( { _id : "stored_func" , value : funcToStore } ) +t.insertOne({'_id': 0, 'myFunc': function() { return 'tesval'; } }); + +assertMongoClientCorrect(); +assertNoStoredWhere(); + +MongoRunner.stopMongod(testServer); +})(); diff --git a/jstests/noPassthrough/js_protection_roundtrip.js b/jstests/noPassthrough/js_protection_roundtrip.js new file mode 100644 index 00000000000..6cd82f81e94 --- /dev/null +++ b/jstests/noPassthrough/js_protection_roundtrip.js @@ -0,0 +1,48 @@ +/** + * Test function roundtripping in documents with --enableJavaScriptProtection. + * + * Ensure that: + * 1. A function stored in a document can be loaded into a Code() + * object in the mongo shell with the --enableJavaScriptProtection flag. + * 2. A Code object is correctly serialized as BSON type 'Code' or + * 'CodeWScope'. + */ +(function() { +"use strict"; + +var testServer = MongoRunner.runMongod({setParameter: 'javascriptProtection=true'}), + db = testServer.getDB("test"), + t = db.foo, + x; + +function makeRoundTrip() { + var mongo = runMongoProgram("mongo", + "--port", testServer.port, + "--enableJavaScriptProtection", + "--eval", + "var x = db.foo.findOne({'_id' : 0});" + + "db.foo.insertOne({'_id': 1, myFunc: x.myFunc});" + + "print(\"completed gracefully\");" + ); + + var mongoOutput = rawMongoProgramOutput(); + assert(!mongoOutput.match(/assert failed/)); + assert(mongoOutput.match(/completed gracefully/)); +} + +/** + * ACTUAL TEST + */ + +t.insertOne({'_id': 0, 'myFunc': function() { return 'yes'; } }); + +makeRoundTrip(); + +x = t.findOne({'_id': 1}); + +if (!x.myFunc() == 'yes') { + assert(0); +} + +MongoRunner.stopMongod(testServer); +})(); diff --git a/src/mongo/scripting/SConscript b/src/mongo/scripting/SConscript index 4f92296f1c7..c27e780985a 100644 --- a/src/mongo/scripting/SConscript +++ b/src/mongo/scripting/SConscript @@ -107,6 +107,7 @@ if usemozjs: 'mozjs/base.cpp', 'mozjs/bindata.cpp', 'mozjs/bson.cpp', + 'mozjs/code.cpp', 'mozjs/countdownlatch.cpp', 'mozjs/cursor.cpp', 'mozjs/cursor_handle.cpp', diff --git a/src/mongo/scripting/engine.h b/src/mongo/scripting/engine.h index dcceb9c07ff..f06333e9a45 100644 --- a/src/mongo/scripting/engine.h +++ b/src/mongo/scripting/engine.h @@ -248,6 +248,9 @@ public: virtual void enableJIT(bool value) = 0; virtual bool isJITEnabled() const = 0; + virtual void enableJavaScriptProtection(bool value) = 0; + virtual bool isJavaScriptProtectionEnabled() const = 0; + static void setup(); static void dropScopeCache(); diff --git a/src/mongo/scripting/mozjs/code.cpp b/src/mongo/scripting/mozjs/code.cpp new file mode 100644 index 00000000000..701ed1d0375 --- /dev/null +++ b/src/mongo/scripting/mozjs/code.cpp @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/scripting/mozjs/code.h" + +#include "mongo/scripting/mozjs/implscope.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/scripting/mozjs/valuereader.h" +#include "mongo/scripting/mozjs/valuewriter.h" +#include "mongo/scripting/mozjs/wrapconstrainedmethod.h" +#include "mongo/stdx/memory.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace mozjs { + +const JSFunctionSpec CodeInfo::methods[2] = { + MONGO_ATTACH_JS_CONSTRAINED_METHOD(toString, CodeInfo), + JS_FS_END, +}; + +const char* const CodeInfo::className = "Code"; + +void CodeInfo::Functions::toString::call(JSContext* cx, JS::CallArgs args) { + ObjectWrapper o(cx, args.thisv()); + + std::string str = str::stream() + << "Code({\"code\":\"" << o.getString(InternedString::code) << "\"," + << "\"scope\":" << o.getObject(InternedString::scope) << "\"})"; + + ValueReader(cx, args.rval()).fromStringData(str); +} + +void CodeInfo::construct(JSContext* cx, JS::CallArgs args) { + uassert(ErrorCodes::BadValue, + "Code needs 0, 1 or 2 arguments", + args.length() == 0 || args.length() == 1 || args.length() == 2); + auto scope = getScope(cx); + + JS::RootedObject thisv(cx); + + scope->getProto<CodeInfo>().newObject(&thisv); + ObjectWrapper o(cx, thisv); + + if (args.length() == 0) { + o.setString(InternedString::code, ""); + } + else if (args.length() == 1) { + JS::HandleValue codeArg = args.get(0); + if (!codeArg.isString()) + uasserted(ErrorCodes::BadValue, "code must be a string"); + + o.setValue(InternedString::code, codeArg); + } + else { + if (!args.get(0).isString()) + uasserted(ErrorCodes::BadValue, "code must be a string"); + if (!args.get(1).isObject()) + uasserted(ErrorCodes::BadValue, "scope must be an object"); + + o.setValue(InternedString::code, args.get(0)); + o.setValue(InternedString::scope, args.get(1)); + } + + args.rval().setObjectOrNull(thisv); +} + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/code.h b/src/mongo/scripting/mozjs/code.h new file mode 100644 index 00000000000..a9d2e5523bb --- /dev/null +++ b/src/mongo/scripting/mozjs/code.h @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include "mongo/scripting/mozjs/wraptype.h" + +namespace mongo { +namespace mozjs { + +/** + * The "Code" Javascript object. + * + * Holds a bson Code or CodeWScope + */ + +struct CodeInfo : public BaseInfo { + static void construct(JSContext* cx, JS::CallArgs args); + + struct Functions { + MONGO_DECLARE_JS_FUNCTION(toString); + }; + + static const JSFunctionSpec methods[2]; + + static const char* const className; +}; + +} // namespace mozjs +} // namespace mongo diff --git a/src/mongo/scripting/mozjs/engine.cpp b/src/mongo/scripting/mozjs/engine.cpp index ac4691ddd4e..54c6c5555e6 100644 --- a/src/mongo/scripting/mozjs/engine.cpp +++ b/src/mongo/scripting/mozjs/engine.cpp @@ -47,6 +47,7 @@ namespace mongo { namespace { MONGO_EXPORT_SERVER_PARAMETER(disableJavaScriptJIT, bool, false); +MONGO_EXPORT_SERVER_PARAMETER(javascriptProtection, bool, false); } // namespace @@ -126,6 +127,14 @@ bool MozJSScriptEngine::isJITEnabled() const { return !disableJavaScriptJIT.load(); } +void MozJSScriptEngine::enableJavaScriptProtection(bool value) { + javascriptProtection.store(value); +} + +bool MozJSScriptEngine::isJavaScriptProtectionEnabled() const { + return javascriptProtection.load(); +} + void MozJSScriptEngine::registerOperation(OperationContext* txn, MozJSImplScope* scope) { stdx::lock_guard<stdx::mutex> giLock(_globalInterruptLock); diff --git a/src/mongo/scripting/mozjs/engine.h b/src/mongo/scripting/mozjs/engine.h index 80d2ee8ec0f..9b3178912e7 100644 --- a/src/mongo/scripting/mozjs/engine.h +++ b/src/mongo/scripting/mozjs/engine.h @@ -66,6 +66,9 @@ public: void enableJIT(bool value) override; bool isJITEnabled() const override; + void enableJavaScriptProtection(bool value) override; + bool isJavaScriptProtectionEnabled() const override; + void registerOperation(OperationContext* ctx, MozJSImplScope* scope); void unregisterOperation(unsigned int opId); diff --git a/src/mongo/scripting/mozjs/implscope.cpp b/src/mongo/scripting/mozjs/implscope.cpp index 52e3af75ca6..9c298b4e52c 100644 --- a/src/mongo/scripting/mozjs/implscope.cpp +++ b/src/mongo/scripting/mozjs/implscope.cpp @@ -169,6 +169,10 @@ OperationContext* MozJSImplScope::getOpContext() const { return _opCtx; } +bool MozJSImplScope::isJavaScriptProtectionEnabled() const { + return _engine->isJavaScriptProtectionEnabled(); +} + bool MozJSImplScope::_interruptCallback(JSContext* cx) { auto scope = getScope(cx); @@ -300,6 +304,7 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine) _hasOutOfMemoryException(false), _binDataProto(_context), _bsonProto(_context), + _codeProto(_context), _countDownLatchProto(_context), _cursorProto(_context), _cursorHandleProto(_context), @@ -738,6 +743,7 @@ void MozJSImplScope::reset() { void MozJSImplScope::installBSONTypes() { _binDataProto.install(_global); _bsonProto.install(_global); + _codeProto.install(_global); _dbPointerProto.install(_global); _dbRefProto.install(_global); _errorProto.install(_global); diff --git a/src/mongo/scripting/mozjs/implscope.h b/src/mongo/scripting/mozjs/implscope.h index de496546042..26fd75fb574 100644 --- a/src/mongo/scripting/mozjs/implscope.h +++ b/src/mongo/scripting/mozjs/implscope.h @@ -34,6 +34,7 @@ #include "mongo/client/dbclientcursor.h" #include "mongo/scripting/mozjs/bindata.h" #include "mongo/scripting/mozjs/bson.h" +#include "mongo/scripting/mozjs/code.h" #include "mongo/scripting/mozjs/countdownlatch.h" #include "mongo/scripting/mozjs/cursor.h" #include "mongo/scripting/mozjs/cursor_handle.h" @@ -106,6 +107,8 @@ public: void gc() override; + bool isJavaScriptProtectionEnabled() const; + double getNumber(const char* field) override; int getNumberInt(const char* field) override; long long getNumberLongLong(const char* field) override; @@ -262,6 +265,12 @@ public: } template <typename T> + typename std::enable_if<std::is_same<T, CodeInfo>::value, WrapType<T>&>::type + getProto() { + return _codeProto; + } + + template <typename T> typename std::enable_if<std::is_same<T, ObjectInfo>::value, WrapType<T>&>::type getProto() { return _objectProto; } @@ -369,6 +378,7 @@ private: WrapType<BinDataInfo> _binDataProto; WrapType<BSONInfo> _bsonProto; + WrapType<CodeInfo> _codeProto; WrapType<CountDownLatchInfo> _countDownLatchProto; WrapType<CursorInfo> _cursorProto; WrapType<CursorHandleInfo> _cursorHandleProto; diff --git a/src/mongo/scripting/mozjs/internedstring.defs b/src/mongo/scripting/mozjs/internedstring.defs index 46bc16b2c54..40bf0b97498 100644 --- a/src/mongo/scripting/mozjs/internedstring.defs +++ b/src/mongo/scripting/mozjs/internedstring.defs @@ -8,6 +8,7 @@ MONGO_MOZJS_INTERNED_STRING(arrayAccess, "arrayAccess") MONGO_MOZJS_INTERNED_STRING(_batchSize, "_batchSize") MONGO_MOZJS_INTERNED_STRING(bottom, "bottom") MONGO_MOZJS_INTERNED_STRING(_bson, "_bson") +MONGO_MOZJS_INTERNED_STRING(code, "code") MONGO_MOZJS_INTERNED_STRING(_collection, "_collection") MONGO_MOZJS_INTERNED_STRING(constructor, "constructor") MONGO_MOZJS_INTERNED_STRING(_cursor, "_cursor") @@ -39,6 +40,7 @@ MONGO_MOZJS_INTERNED_STRING(_options, "_options") MONGO_MOZJS_INTERNED_STRING(_query, "_query") MONGO_MOZJS_INTERNED_STRING(readOnly, "readOnly") MONGO_MOZJS_INTERNED_STRING(_ro, "_ro") +MONGO_MOZJS_INTERNED_STRING(scope, "scope") MONGO_MOZJS_INTERNED_STRING(_shortName, "_shortName") MONGO_MOZJS_INTERNED_STRING(singleton, "singleton") MONGO_MOZJS_INTERNED_STRING(_skip, "_skip") diff --git a/src/mongo/scripting/mozjs/valuereader.cpp b/src/mongo/scripting/mozjs/valuereader.cpp index c80efc017da..630d446a4d0 100644 --- a/src/mongo/scripting/mozjs/valuereader.cpp +++ b/src/mongo/scripting/mozjs/valuereader.cpp @@ -53,13 +53,35 @@ void ValueReader::fromBSONElement(const BSONElement& elem, const BSONObj& parent switch (elem.type()) { case mongo::Code: - scope->newFunction(elem.valueStringData(), _value); + // javascriptProtection prevents Code and CodeWScope BSON types from + // being automatically marshalled into executable functions. + if (scope->isJavaScriptProtectionEnabled()) { + JS::AutoValueArray<1> args(_context); + ValueReader(_context, args[0]).fromStringData(elem.valueStringData()); + + JS::RootedObject obj(_context); + scope->getProto<CodeInfo>().newInstance(args, _value); + } + else { + scope->newFunction(elem.valueStringData(), _value); + } return; case mongo::CodeWScope: - if (!elem.codeWScopeObject().isEmpty()) - warning() << "CodeWScope doesn't transfer to db.eval"; - scope->newFunction(StringData(elem.codeWScopeCode(), elem.codeWScopeCodeLen() - 1), - _value); + if (scope->isJavaScriptProtectionEnabled()) { + JS::AutoValueArray<2> args(_context); + + ValueReader(_context, args[0]).fromStringData(elem.valueStringData()); + ValueReader(_context, args[1]).fromBSON(elem.codeWScopeObject(), + nullptr, readOnly); + + scope->getProto<CodeInfo>().newInstance(args, _value); + } + else { + if (!elem.codeWScopeObject().isEmpty()) + warning() << "CodeWScope doesn't transfer to db.eval"; + scope->newFunction(StringData(elem.codeWScopeCode(), elem.codeWScopeCodeLen() - 1), + _value); + } return; case mongo::Symbol: case mongo::String: diff --git a/src/mongo/scripting/mozjs/valuewriter.cpp b/src/mongo/scripting/mozjs/valuewriter.cpp index 080e8317bbf..967a2947cd8 100644 --- a/src/mongo/scripting/mozjs/valuewriter.cpp +++ b/src/mongo/scripting/mozjs/valuewriter.cpp @@ -266,6 +266,27 @@ void ValueWriter::_writeObject(BSONObjBuilder* b, return; } + if (scope->getProto<CodeInfo>().getJSClass() == jsclass) { + if (o.hasOwnField(InternedString::scope) // CodeWScope + && o.type(InternedString::scope) == mongo::Object) { + + if (o.type(InternedString::code) != mongo::String) { + uasserted(ErrorCodes::BadValue, "code must be a string"); + } + + b->appendCodeWScope(sd, o.getString(InternedString::code), + o.getObject(InternedString::scope)); + } + else { // Code + if (o.type(InternedString::code) != mongo::String) { + uasserted(ErrorCodes::BadValue, "code must be a string"); + } + + b->appendCode(sd, o.getString(InternedString::code)); + } + return; + } + if (scope->getProto<NumberDecimalInfo>().getJSClass() == jsclass) { b->append(sd, NumberDecimalInfo::ToNumberDecimal(_context, obj)); diff --git a/src/mongo/shell/db.js b/src/mongo/shell/db.js index e17cfa36858..91892ff55e3 100644 --- a/src/mongo/shell/db.js +++ b/src/mongo/shell/db.js @@ -1155,7 +1155,12 @@ DB.prototype.getQueryOptions = function() { DB.prototype.loadServerScripts = function(){ var global = Function('return this')(); this.system.js.find().forEach(function(u) { - global[u._id] = u.value; + if (u.value.constructor === Code) { + global[u._id] = eval("(" + u.value.code + ")"); + } + else { + global[u._id] = u.value; + } }); }; diff --git a/src/mongo/shell/dbshell.cpp b/src/mongo/shell/dbshell.cpp index 22b9e22d91d..c064c292e83 100644 --- a/src/mongo/shell/dbshell.cpp +++ b/src/mongo/shell/dbshell.cpp @@ -683,6 +683,7 @@ int _main(int argc, char* argv[], char** envp) { mongo::ScriptEngine::setup(); mongo::globalScriptEngine->setScopeInitCallback(mongo::shell_utils::initScope); mongo::globalScriptEngine->enableJIT(!shellGlobalParams.nojit); + mongo::globalScriptEngine->enableJavaScriptProtection(shellGlobalParams.javascriptProtection); auto poolGuard = MakeGuard([] { ScriptEngine::dropScopeCache(); }); diff --git a/src/mongo/shell/shell_options.cpp b/src/mongo/shell/shell_options.cpp index 62ce19c12a3..5094f01276c 100644 --- a/src/mongo/shell/shell_options.cpp +++ b/src/mongo/shell/shell_options.cpp @@ -124,6 +124,11 @@ Status addMongoShellOptions(moe::OptionSection* options) { moe::Switch, "disable the Javascript Just In Time compiler"); + options->addOptionChaining("enableJavaScriptProtection", + "enableJavaScriptProtection", + moe::Switch, + "disable automatic JavaScript function marshalling"); + Status ret = Status::OK(); #ifdef MONGO_CONFIG_SSL ret = addSSLClientOptions(options); @@ -260,6 +265,9 @@ Status storeMongoShellOptions(const moe::Environment& params, if (params.count("nodb")) { shellGlobalParams.nodb = true; } + if (params.count("enableJavaScriptProtection")) { + shellGlobalParams.javascriptProtection = true; + } if (params.count("norc")) { shellGlobalParams.norc = true; } diff --git a/src/mongo/shell/shell_options.h b/src/mongo/shell/shell_options.h index 2e94ff92282..4699f3d09f4 100644 --- a/src/mongo/shell/shell_options.h +++ b/src/mongo/shell/shell_options.h @@ -62,6 +62,7 @@ struct ShellGlobalParams { bool nodb; bool norc; bool nojit = false; + bool javascriptProtection = false; std::string script; |