Subj : Class protection from multiple script execution using the same runtime To : netscape.public.mozilla.jseng From : Matthew Mondor Date : Thu Jul 22 2004 03:17 am This is a multi-part message in MIME format. --------------090005000902030601020900 Content-Type: text/plain; charset=us-ascii; format=flowed Content-Transfer-Encoding: 7bit Hi all, I am relatively new to SpiderMonkey, and have been testing some things to evaluate how powerful and secure it could be. I am surprised of the flexibility of the system, and it seems espcialyl suited to some of my projects instead of using something like jvm, python, perl or other existing language embedded in C applications. In an ideal setup in one of my projects, I would use a pool of already setup contexts, which can be recycled for multiple script execution. To achieve this, I must ensure that user scripts cannot modify "native application" classes. So I have attempted several ways of preventing scripts to add properties and/or methods to a system class. I here attach both the test C source and the little accompanying test javascript program. As can be seen, the test script manages to add properties and methods to the API test class. I have tested various methods to prevent this, some of which prevented the C program from setting the properties and methods as well :), but that was not intended . Thanks, Matt --------------090005000902030601020900 Content-Type: text/plain; name="test.c" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="test.c" /* $Id: test.c,v 1.11 2004/07/16 02:12:52 mmondor Exp $ */ /* * Copyright (c) 2004, Matthew Mondor * ALL RIGHTS RESERVED. * * Test using SpiderMonkey (Mozilla's JavaScript implementation). * Proof of concept for 3s4i Inc. * * Reasons: * - More and more people know Java, and JavaScript ressembles Java alot. * - JavaScript was the first language to officially be used for the WWW, * and is thus very well known by most Web sites maintainers. * - The system is object-oriented, and allows read-only object fields. * It also comports a Garbage Collector. * - Much smaller than Java. More easily used for embedded products. * Alot less default classes. The standard set of functions (ECMA 262) is * reasonable and restrictive enough as far as security is concerned. * - C-like syntax, powerful enough to be suitable for many applications. * - Was created from the ground up for such embedding in C applications. * Also was tested successfully for many years into Netscape and Mozilla * browsers. * - Allows both applications to provide C native functions to administration * scripts, as well as scripts to define functions for events callbacks * triggered from C. * - With JS_SetBranchCallback(), the application always has full control, * even if a script is running in an endless loop. * - SpiderMonkey already distributed separately than with Mozilla as a * standalone, easily compilable and portable source tarball. * - Faster administration of embedded units preventing the need for huge * and slow Java Development Kit and avoiding it's compilation phases. * Moreover, the implementation costs of specially tailored, non-standard * mini-Java VM implementations are avoided. This also is true of licensing. * * TODO: * - Get JS_SetPrivate() and JS_GetPrivate() to work for properties. * Have a constructor create the private data, and the destructor free it. * - Create another class/object which is suitable for use with 'new' in * scripts, as opposed to one which is already static for the whole script, * like API. The new one for instance could represent a file, and have * properties for size, type etc... as well as a few methods for operations * on said file. * - Modularize runtime and application-specific classes stuff in a well * presented library. Use mmpool(3) to prepare runtimes and invoke them * efficiently afterwards. * - Add some JS debugging support as such to detect and report error sources. * - Prepare presentation with "Reasons" section above as well as flowcharts * demonstrating the native functions calls and script functions callbacks. * Demonstrate that the main API provided for the Logatec devices will * require minimal changes only (if any). Also how they no longer will waste * time using VB and fixing multipurpose computers at customers to use the * expected VB version and risk that a secretary or employee there ruins * everything or windows shits itself as usual after some time. Custom * devices will be installed in no time and will be dedicated to their tasks, * as well as non-accessible by the office people. Emphasize on how much * nonsense this is, especially when they are a pay management company! * Remind audience of what happened two years ago when system failed and * pays for many companies were delayed for more than a month when admins * were stuck fixing Win32/VB setups remotely using modems and PCAnywhere! * Once a working setup, demonstrate new remote administration facilities * as well as easy upgrade process. */ #include #include #include #include #include #include #include #include #include #include /* Size runtime objects must take to run the GC */ #define GCBYTES 1048576 /* 1MB */ /* Size of stack to allocate for every context */ #define STACKBYTES 8192 /* 8KB */ /* Structure used to load files */ typedef struct { void *data; size_t size; } file_t; int main(int, char **); static file_t *file_load(const char *); static void file_free(file_t *); static JSBool branch_callback(JSContext *, JSScript *); static int class_property_add(JSContext *, JSObject *, jsval, jsval *); static JSBool api_print(JSContext *, JSObject *, uintN, jsval *, jsval *); static JSBool api_file_size(JSContext *, JSObject *, uintN, jsval *, jsval *); static JSBool api_property_get(JSContext *, JSObject *, jsval, jsval *); static JSBool api_property_set(JSContext *, JSObject *, jsval, jsval *); /* Defaults for the global class */ static JSClass global_class = { "global", 0, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub }; /* * API class */ /* List of allowed property IDs */ enum api_properties { APIP_PROPERTY1 = 0, APIP_PROPERTY2, APIP_PROPERTY3, APIP_MAX }; /* Defaults for the API class */ static JSClass api_class = { "API", JSCLASS_HAS_PRIVATE, /*class_property_add*/JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub }; /* The array of function/method definitions for API class */ static JSFunctionSpec api_functions[] = { { "print", api_print, 1, 0, 0 }, { "fileSize", api_file_size, 1, 0, 0 }, { NULL, NULL, 0, 0, 0 } }; /* The array of property definitions for API class */ static JSPropertySpec api_properties[] = { { "property1", APIP_PROPERTY1, JSPROP_PERMANENT, api_property_get, api_property_set }, { "property2", APIP_PROPERTY2, JSPROP_PERMANENT, api_property_get, api_property_set }, { "property3", APIP_PROPERTY3, JSPROP_PERMANENT | JSPROP_READONLY, api_property_get, api_property_set }, { NULL, 0, 0, NULL, NULL } }; /* Properties array */ static jsval api_property_jsval[APIP_MAX]; /* * File class */ /* enum file_properties { FILEP_SIZE = 0, FILEP_NAME, FILEP_EXISTS, FILEP_MAX }; static JSClass file_class = { "File", JSCLASS_HAS_PRIVATE, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub }; static JSFunctionSpec file_functions[] = { { "unlink", file_method_unlink, 0, 0, 0 }, { "rename", file_method_rename, 1, 0, 0 }, { NULL, NULL, 0, 0, 0 } }; static JSPropertySpec file_properties[] = { { "size", FILEP_SIZE, JSPROP_PERMANENT | JSPROP_READONLY, file_property_size_get, NULL }, { "name", FILEP_NAME, JSPROP_PERMANENT, file_property_name_get, file_property_name_set }, { "exists", FILEP_EXISTS, JSPROP_PERMANENT | JSPROP_READONLY, file_property_exists_get, NULL }, { NULL, 0, 0, NULL, NULL } }; static jsval file_property_jsval[FILEP_MAX]; */ static JSRuntime *rt; static JSObject *global, *api/*, *file*/; /* static JSBool protect = JS_FALSE; */ int main(int argc, char **argv) { file_t *file; JSContext *ctx; JSObject *class; uintN prop_flags; JSBool prop_found; int i; if (argc != 2) { (void) fprintf(stderr, "Usage: test \n"); exit(EXIT_FAILURE); } if ((file = file_load(argv[1])) == NULL) { (void) fprintf(stderr, "Error loading '%s'\n", argv[1]); exit(EXIT_FAILURE); } /* * We always need at least one runtime per process, at least one * context per thread and at least a global object per context * (standard classes, like Date). */ if ((rt = JS_NewRuntime(GCBYTES)) == NULL || (ctx = JS_NewContext(rt, STACKBYTES)) == NULL || (global = JS_NewObject(ctx, &global_class, NULL, NULL)) == NULL || (!JS_InitStandardClasses(ctx, global))) { file_free(file); (void) fprintf(stderr, "Error initializing runtime\n"); exit(EXIT_FAILURE); } /* * This is a very useful and important feature, enable our callback * function which will get called whenever the script branches * backwards, returns from a function or exits. It allows us to * even maintain control in cases where the script loops endlessly. */ (void) JS_SetBranchCallback(ctx, branch_callback); /* * Attach our native API functions. These are attached on the global * object, which means that they need not be accessed via a specific * object/class. As this code demonstrates, this is very easy to do. */ /* if (!JS_DefineFunctions(ctx, global, api_functions)) { (void) fprintf(stderr, "Error attaching functions to global"); exit(EXIT_FAILURE); } */ /* * Create a custom class named API and attach our custom methods * (functions) and properties (variables) to it. This will allow * user scripts to call in this case: API.print(), API.fileSize(). * This is slightly more complex. */ /* Create prototype object, child of global object */ if ((api = JS_NewObject(ctx, &api_class, NULL, global)) == NULL) { (void) fprintf(stderr, "Error initializing API object\n"); exit(EXIT_FAILURE); } /* Define our methods in the prototype object */ if (!JS_DefineFunctions(ctx, api, api_functions)) { (void) fprintf(stderr, "Error defining API functions\n"); exit(EXIT_FAILURE); } /* Define our properties in the prototype object */ if (!JS_DefineProperties(ctx, api, api_properties)) { (void) fprintf(stderr, "Error defining API properties\n"); exit(EXIT_SUCCESS); } /* Make sure that GC doesn't free/destroy this object */ if (!JS_AddNamedRoot(ctx, api, "API_Prototype")) { (void) fprintf(stderr, "Error adding API to root\n"); exit(EXIT_FAILURE); } /* * Make sure that it is not possible to add properties to this object * from user code, overriding the addProperty() class method. */ /* * XXX Seems to have no effect! It works if I set it in the api_class * structure directly from the start, but then I cannot add properties * in the above code. And if I use a boolean flag to change the * behavior of class_property_add() after executing the above code, * it appears that even setting properties fail in user code, for some * reason (although logically only setProperty() method should be * invoked in this case?) */ api_class.addProperty = class_property_add; /* protect = JS_TRUE;*/ /* Finally initialize our new class */ if ((class = JS_InitClass(ctx, global, api, &api_class, NULL, 0, NULL, NULL, NULL, NULL)) == NULL) { (void) fprintf(stderr, "Error initializing API class\n"); exit(EXIT_FAILURE); } /* * Make sure that noone can delete our class, setting the permanent * and read only flags. */ if (JS_GetPropertyAttributes(ctx, global, "API", &prop_flags, &prop_found) && prop_found) (void) JS_SetPropertyAttributes(ctx, global, "API", prop_flags | JSPROP_PERMANENT | JSPROP_READONLY, &prop_found); else { (void) fprintf(stderr, "Error modifying API attributes\n"); exit(EXIT_FAILURE); } /* * Set object private data for our test properties. * Just use a global jsval array for now, which allows values of * different types to be assigned. Once refined, we shall allocate * the private data so that each context has it's own instance. */ for (i = 0; i < APIP_MAX; i++) api_property_jsval[i] = INT_TO_JSVAL(-1); (void) JS_SetPrivate(ctx, class, api_property_jsval); /* * Now execute script loaded into our file_t. * We simplify this process by calling JS_EvaluateScript() which * will first tokenize/compile the result, and then interpret/run it. * Moreover, it allows the script to optionally return a value * directly like if it was a function. * Alternatively, we could use JS_CompileFile() or JS_CompileScript() * to pre-tokenize the script, and JS_ExecuteScript() to interpret it. */ { jsval rval, pval; JSString *str; int i; if (JS_EvaluateScript(ctx, global, file->data, file->size, argv[1], 1, &rval)) { str = JS_ValueToString(ctx, rval); (void) printf("Script result: %s\n", JS_GetStringBytes(str)); /* * Attempt to call JS function "callMe" if the script * created it. */ for (i = 0; i < 10; i++) { pval = INT_TO_JSVAL(i); if (!JS_CallFunctionName(ctx, global, "callMe", 1, &pval, &rval)) break; } } } /* Cleanup */ file_free(file); JS_RemoveRoot(ctx, api); JS_DestroyContext(ctx); JS_DestroyRuntime(rt); exit(EXIT_SUCCESS); } /* Loads specified file and returns a file_t pointer. */ static file_t * file_load(const char *filename) { int fd; struct stat st; file_t *file; assert(filename != NULL); if ((fd = open(filename, O_RDONLY)) != -1) { if (fstat(fd, &st) == 0) { if ((file = malloc(sizeof(file_t))) != NULL) { file->size = (size_t)st.st_size; if ((file->data = mmap(NULL, file->size, PROT_READ, MAP_FILE, fd, 0)) != MAP_FAILED) { (void) close(fd); return file; } free(file); } } (void) close(fd); } return NULL; } /* Frees a file_t which was returned by a previous file_load() call. */ static void file_free(file_t *file) { assert (file != NULL); (void) munmap(file->data, file->size); free(file); } /* * This function is called during the execution of the script so that we can * remain in control of the application. If we only allow the scripts to * define functions for callbacks, we can use the first instance if this event * to abort the script if wanted, as well. We can then set an alternative * callback function and execute the script provided functions at specific * events. Of course, it also would be possible to use setitimer(2) to have * a SIGALRM signal trigger a function at regular set intervals. */ /* ARGSUSED */ static JSBool branch_callback(JSContext *ctx, JSScript *script) { jsval pval, rval; static int count = 0; (void) printf("branch_callback()\n"); /* Call callMe() script-provided handler if any, as a test */ pval = INT_TO_JSVAL(count++); (void) JS_CallFunctionName(ctx, global, "callMe", 1, &pval, &rval); /* Returning JS_FALSE here aborts the script */ return JS_TRUE; } /* * Used to override the default addProperty() method of a class. * Permits to prevent user scripts from adding properties into persistent * objects which would remain afterwards. It is not necessary to override * the delProperty() method, since we simply specify the needed flag on * properties which are not allowed to be deleted. */ /* ARGSUSED */ static int class_property_add(JSContext *ctx, JSObject *obj, jsval v1, jsval *v2) { /* Previous test return !protect; */ /* Previous test if (protect) return JS_FALSE; return JS_PropertyStub(ctx, obj, v1, v2); */ return (obj != api); } /* A test function which we want to provide our JS API with. */ static JSBool api_print(JSContext *ctx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { JSString *str; /* * Arguments are in argc, argv and we must return our return value * into rval. */ /* * If we do not perform this check, the caller is allowed to supply * more arguments than we expect, or to supply none at all, in which * case the argument we expect is undefined. Returning JS_FALSE here * causes script execution to be aborted. Note that JS_ValueToString() * always returns a JSString pointer, and that we do not need to free * the JSString buffer, since it becomes subject to the GC. This also * means that if we needed to store the value for some time we would * either need to tag it to not be subject to the GC, or to copy the * result in an application-specific buffer. */ if (argc != 1) return JS_FALSE; str = JS_ValueToString(ctx, *argv); (void) printf("%s", JS_GetStringBytes(str)); return JS_TRUE; } /* * Another test function, which allows the script to obtain a result from us. * In this case, we return the size of the requested file, or -1. */ static JSBool api_file_size(JSContext *ctx, JSObject *obj, uintN argc, jsval *argv, jsval *ret) { JSString *str; struct stat st; ssize_t size; if (argc != 1) return JS_FALSE; str = JS_ValueToString(ctx, *argv); if (stat(JS_GetStringBytes(str), &st) == 0) size = (ssize_t)st.st_size; else size = -1; *ret = INT_TO_JSVAL((int)size); return JS_TRUE; } /* * Functions to get and set API.property, as an example. It is possible to use * the same functions for multiple peoperties, and to use the 8-bit unsigned * unique ID of the property to distinguish them. It is possible to use object- * specific private data as well, using JS_SetPrivate() and JS_GetPrivate(). * In the case of read-only properties, the api_property_set() function is * simply not called. */ static JSBool api_property_get(JSContext *ctx, JSObject *obj, jsval id, jsval *vp) { jsval *val; int p; p = JSVAL_TO_INT(id); assert(p > -1 && p < APIP_MAX); val = JS_GetPrivate(ctx, obj); *vp = val[p]; return JS_TRUE; } static JSBool api_property_set(JSContext *ctx, JSObject *obj, jsval id, jsval *vp) { jsval *val; int p; p = JSVAL_TO_INT(id); assert(p > -1 && p < APIP_MAX); val = JS_GetPrivate(ctx, obj); val[p] = *vp; return JS_TRUE; } --------------090005000902030601020900 Content-Type: application/x-javascript; name="test.js" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="test.js" /* $Id: test.js,v 1.7 2004/07/16 02:12:52 mmondor Exp $ */ /* * Oops, the following test should not succeed on API class! */ /* {{ */ function test(s) { API.print(s); } API.test = test; API.test("Successfully added method to API class!\n"); API.test2 = "Successfully added property to API class!\n"; API.test(API.test2 + "\n"); /* }} */ /* * These tests should succeed, however. */ API.print("API.property1 = " + API.property1 + "\n"); API.print("API.property2 = " + API.property2 + "\n"); API.print("API.property3 = " + API.property3 + "\n\n"); /* * Why do those fail if addProperty() method is overriden by one returning * JS_FALSE? They exist and thus should only call the setProperty() method... */ API.property1 = 'Hello'; API.property2 = 911; /* * Following statement should fail, but is simply internally ignored at least * (the setProperty() method is not called). */ API.property3 = 'READONLY!'; API.print("API.property1 = " + API.property1 + "\n"); API.print("API.property2 = " + API.property2 + "\n"); API.print("API.property3 = " + API.property3 + "\n\n"); /* * Perform a test loop, during which callMe() should be called by the * application code, via the break callback handler function. */ for (i = 0; i < 3; i++) { API.print("We are the: " + Date() + "\n"); API.print("NetBSD Kernel size is " + (API.fileSize("/netbsd") / 1024 / 1024) + "MB\n"); API.print("END\n"); } API.print("\n"); /* * Will be called by our application if we create it */ function callMe(n) { API.print("CallMe(" + n + ")!\n"); } /* * Interesting feature where we can return a value explicitely, optionally */ 10 --------------090005000902030601020900-- .