07 - 嵌入与安全
嵌入与安全
本章介绍如何将 QuickJS 安全地嵌入到应用程序中,包括沙箱隔离、资源限制和中断控制。
7.1 嵌入架构
典型嵌入架构
┌──────────────────────────────────────────────────────┐
│ 宿主应用程序 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 安全层 (C/C++) │ │
│ │ - API 白名单 - 资源限制 - 审计日志 │ │
│ └────────────────────┬────────────────────────────┘ │
│ ┌────────────────────┴────────────────────────────┐ │
│ │ JSContext (沙箱) │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ JavaScript 代码 │ │ │
│ │ │ - 业务逻辑 │ │ │
│ │ │ - 数据处理 │ │ │
│ │ │ - 规则引擎 │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ 受限的标准库 │ │ │
│ │ │ - console.log - Math │ │ │
│ │ │ - JSON - Array/String/Object │ │ │
│ │ │ ✗ 文件系统 ✗ 网络 │ │ │
│ │ │ ✗ 进程 ✗ 操作系统 │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
7.2 沙箱环境构建
移除危险 API
// sandbox.c — 构建安全的 JavaScript 沙箱
#include "quickjs-libc.h"
#include <stdio.h>
#include <string.h>
// 不安全的全局对象列表
static const char *blocked_globals[] = {
"std", // 文件操作
"os", // 系统调用
"scriptArgs", // 脚本参数(可能泄露信息)
"print", // 原始输出
"__loadScript", // 脚本加载
NULL
};
// 移除危险的全局属性
static void remove_dangerous_globals(JSContext *ctx) {
JSValue global = JS_GetGlobalObject(ctx);
for (const char **name = blocked_globals; *name; name++) {
JS_DeletePropertyStr(ctx, global, *name, JS_PROP_THROW);
}
// 修改 console 对象(保留 log,移除其他)
JSValue console = JS_GetPropertyStr(ctx, global, "console");
if (!JS_IsUndefined(console)) {
// 移除 console.warn, console.error 等(可选)
// JS_DeletePropertyStr(ctx, console, "warn", 0);
// JS_DeletePropertyStr(ctx, console, "error", 0);
}
JS_FreeValue(ctx, console);
JS_FreeValue(ctx, global);
}
// 创建安全的 console.log 实现
static JSValue safe_console_log(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
// 在生产中,可以将输出重定向到日志系统
// 这里简单打印到 stdout
for (int i = 0; i < argc; i++) {
if (i > 0) printf(" ");
const char *str = JS_ToCString(ctx, argv[i]);
if (str) {
printf("%s", str);
JS_FreeCString(ctx, str);
}
}
printf("\n");
return JS_UNDEFINED;
}
// 创建沙箱上下文
JSContext* create_sandbox(JSRuntime *rt) {
JSContext *ctx = JS_NewContext(rt);
// 不注册 std/os 模块(关键!)
// js_init_module_std(ctx, "std"); // ← 不要调用
// js_init_module_os(ctx, "os"); // ← 不要调用
// 移除危险全局
remove_dangerous_globals(ctx);
// 提供安全的替代 API
JSValue global = JS_GetGlobalObject(ctx);
JSValue console = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, console, "log",
JS_NewCFunction(ctx, safe_console_log, "log", 1));
JS_SetPropertyStr(ctx, global, "console", console);
JS_FreeValue(ctx, global);
return ctx;
}
int main() {
JSRuntime *rt = JS_NewRuntime();
// 设置全局资源限制
JS_SetMemoryLimit(rt, 16 * 1024 * 1024); // 16MB 内存
JS_SetMaxStackSize(rt, 256 * 1024); // 256KB 栈
JSContext *ctx = create_sandbox(rt);
// 执行不受信任的代码
const char *untrusted_code = R"(
// 这段代码不能访问文件系统或操作系统
function processData(input) {
return input
.filter(x => x > 0)
.map(x => x * 2)
.reduce((a, b) => a + b, 0);
}
const result = processData([1, -2, 3, -4, 5]);
console.log("Result:", result); // 18
// 这些会失败:
// import * as std from "std"; // 错误:模块不存在
// os.exit(0); // 错误:os 未定义
)";
JSValue result = JS_Eval(ctx, untrusted_code, strlen(untrusted_code),
"<sandbox>", 0);
if (JS_IsException(result)) {
JSValue ex = JS_GetException(ctx);
const char *msg = JS_ToCString(ctx, ex);
fprintf(stderr, "Error: %s\n", msg);
JS_FreeCString(ctx, msg);
JS_FreeValue(ctx, ex);
}
JS_FreeValue(ctx, result);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
7.3 执行超时控制
中断回调机制
// timeout.c — 使用中断回调实现执行超时
#include "quickjs-libc.h"
#include <stdio.h>
#include <signal.h>
#include <time.h>
// 超时上下文
typedef struct {
clock_t start_time;
double timeout_seconds;
volatile int timed_out;
} TimeoutContext;
// 全局超时上下文(信号处理器需要)
static TimeoutContext g_timeout;
// 中断回调(QuickJS 每执行若干条指令后调用)
static int interrupt_handler(JSRuntime *rt, void *opaque) {
TimeoutContext *tc = (TimeoutContext *)opaque;
double elapsed = (double)(clock() - tc->start_time) / CLOCKS_PER_SEC;
if (elapsed > tc->timeout_seconds) {
tc->timed_out = 1;
return 1; // 返回 1 表示中断执行
}
return 0; // 返回 0 表示继续执行
}
// 带超时的代码执行
JSValue eval_with_timeout(JSContext *ctx, const char *code,
double timeout_sec) {
JSRuntime *rt = JS_GetRuntime(ctx);
// 设置超时上下文
g_timeout.start_time = clock();
g_timeout.timeout_seconds = timeout_sec;
g_timeout.timed_out = 0;
// 注册中断回调
JS_SetInterruptHandler(rt, interrupt_handler, &g_timeout);
// 执行代码
JSValue result = JS_Eval(ctx, code, strlen(code), "<timeout>", 0);
// 移除中断回调
JS_SetInterruptHandler(rt, NULL, NULL);
// 检查是否超时
if (g_timeout.timed_out && JS_IsException(result)) {
// 超时产生的异常
JSValue ex = JS_GetException(ctx);
// 替换为友好的超时错误信息
JS_FreeValue(ctx, ex);
JSValue timeout_err = JS_NewError(ctx);
JS_SetPropertyStr(ctx, timeout_err, "message",
JS_NewString(ctx, "Script execution timed out"));
JS_SetPropertyStr(ctx, timeout_err, "code",
JS_NewInt32(ctx, -1));
return JS_Throw(ctx, timeout_err);
}
return result;
}
int main() {
JSRuntime *rt = JS_NewRuntime();
JS_SetMemoryLimit(rt, 32 * 1024 * 1024);
JSContext *ctx = JS_NewContext(rt);
js_init_module_std(ctx, "std");
js_init_module_os(ctx, "os");
// 测试 1:正常执行(应该完成)
printf("Test 1: Normal execution...\n");
JSValue r1 = eval_with_timeout(ctx, "1 + 2", 1.0);
if (!JS_IsException(r1)) {
int32_t val;
JS_ToInt32(ctx, &val, r1);
printf("Result: %d\n", val);
}
JS_FreeValue(ctx, r1);
// 测试 2:无限循环(应该超时)
printf("Test 2: Infinite loop (1s timeout)...\n");
JSValue r2 = eval_with_timeout(ctx,
"while(true) { /* 死循环 */ }", 1.0);
if (JS_IsException(r2)) {
JSValue ex = JS_GetException(ctx);
JSValue msg = JS_GetPropertyStr(ctx, ex, "message");
const char *s = JS_ToCString(ctx, msg);
printf("Caught: %s\n", s);
JS_FreeCString(ctx, s);
JS_FreeValue(ctx, msg);
JS_FreeValue(ctx, ex);
}
JS_FreeValue(ctx, r2);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
7.4 内存限制
设置内存上限
// memory_limit.c — 内存限制详解
#include "quickjs-libc.h"
#include <stdio.h>
int main() {
JSRuntime *rt = JS_NewRuntime();
// 1. 设置内存限制(malloc 上限)
JS_SetMemoryLimit(rt, 4 * 1024 * 1024); // 4MB
// 2. 设置栈大小
JS_SetMaxStackSize(rt, 128 * 1024); // 128KB
JSContext *ctx = JS_NewContext(rt);
// 正常分配(应该成功)
printf("Test 1: Small allocation...\n");
JSValue r1 = JS_Eval(ctx,
"let arr = []; for(let i=0; i<1000; i++) arr.push(i); arr.length",
63, "<mem>", 0);
if (!JS_IsException(r1)) {
int32_t len;
JS_ToInt32(ctx, &len, r1);
printf("Array length: %d\n", len);
}
JS_FreeValue(ctx, r1);
// 大量分配(会触发内存限制)
printf("Test 2: Large allocation (should fail)...\n");
JSValue r2 = JS_Eval(ctx,
"let arr = []; for(let i=0; i<10000000; i++) arr.push('x'.repeat(100))",
82, "<mem>", 0);
if (JS_IsException(r2)) {
JSValue ex = JS_GetException(ctx);
const char *msg = JS_ToCString(ctx, ex);
printf("Caught: %s\n", msg);
JS_FreeCString(ctx, msg);
JS_FreeValue(ctx, ex);
}
JS_FreeValue(ctx, r2);
// 监控内存使用
JSMemoryUsage usage;
JS_ComputeMemoryUsage(rt, &usage);
printf("Current memory usage: %zu bytes\n",
(size_t)usage.malloc_size);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
动态内存限制
// 动态调整内存限制
void adjust_memory_limit(JSRuntime *rt, size_t current_usage) {
// 根据使用情况动态调整
if (current_usage > 100 * 1024 * 1024) {
// 超过 100MB,收紧限制
JS_SetMemoryLimit(rt, current_usage + 10 * 1024 * 1024);
}
}
7.5 递归限制
// recursion_limit.c — 防止栈溢出
#include "quickjs-libc.h"
#include <stdio.h>
int main() {
JSRuntime *rt = JS_NewRuntime();
JS_SetMaxStackSize(rt, 64 * 1024); // 64KB 栈
JSContext *ctx = JS_NewContext(rt);
// 深度递归(会触发栈溢出保护)
const char *code = R"(
function deep(n) {
if (n <= 0) return 0;
return 1 + deep(n - 1);
}
try {
// 尝试深度递归
const result = deep(100000);
console.log("Result:", result);
} catch (e) {
console.log("Stack overflow caught at depth");
// QuickJS 会抛出 "stack overflow" 错误
}
)";
JSValue result = JS_Eval(ctx, code, strlen(code), "<recursion>", 0);
if (JS_IsException(result)) {
JSValue ex = JS_GetException(ctx);
const char *msg = JS_ToCString(ctx, ex);
printf("Error: %s\n", msg);
JS_FreeCString(ctx, msg);
JS_FreeValue(ctx, ex);
}
JS_FreeValue(ctx, result);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
7.6 操作码计数限制
// opcode_limit.c — 限制执行的指令数量
#include "quickjs-libc.h"
#include <stdio.h>
int main() {
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
// 使用 qjs 的 --opcode-count 选项等价的 C API
// 注意:这是通过 interrupt handler 实现的
long opcode_count = 0;
long max_opcodes = 1000000;
// 自定义中断处理器
static int opcode_interrupt(JSRuntime *rt, void *opaque) {
long *count = (long *)opaque;
(*count)++;
return (*count > max_opcodes) ? 1 : 0;
}
JS_SetInterruptHandler(rt, opcode_interrupt, &opcode_count);
// 这段代码会产生大量操作码
const char *code = R"(
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += i;
}
sum;
)";
JSValue result = JS_Eval(ctx, code, strlen(code), "<opcode>", 0);
if (JS_IsException(result)) {
printf("Execution interrupted after %ld opcodes\n", opcode_count);
JSValue ex = JS_GetException(ctx);
const char *msg = JS_ToCString(ctx, ex);
printf("Error: %s\n", msg);
JS_FreeCString(ctx, msg);
JS_FreeValue(ctx, ex);
} else {
int32_t val;
JS_ToInt32(ctx, &val, result);
printf("Completed: sum = %d (opcodes: %ld)\n", val, opcode_count);
}
JS_FreeValue(ctx, result);
JS_SetInterruptHandler(rt, NULL, NULL);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
7.7 多租户隔离
// multi_tenant.c — 多租户 JavaScript 隔离执行
#include "quickjs-libc.h"
#include <stdio.h>
#include <string.h>
typedef struct {
char tenant_id[64];
size_t memory_limit;
double timeout_seconds;
} TenantConfig;
// 每个租户独立的执行环境
typedef struct {
TenantConfig config;
JSRuntime *rt;
JSContext *ctx;
size_t execution_count;
} TenantSandbox;
// 创建租户沙箱
TenantSandbox* create_tenant_sandbox(const TenantConfig *config) {
TenantSandbox *sandbox = malloc(sizeof(TenantSandbox));
sandbox->config = *config;
sandbox->execution_count = 0;
// 每个租户独立的 Runtime(完全隔离)
sandbox->rt = JS_NewRuntime();
JS_SetMemoryLimit(sandbox->rt, config->memory_limit);
JS_SetMaxStackSize(sandbox->rt, 128 * 1024);
sandbox->ctx = JS_NewContext(sandbox->rt);
// 不注册任何系统模块
return sandbox;
}
// 在租户沙箱中执行代码
JSValue tenant_eval(TenantSandbox *sandbox, const char *code) {
sandbox->execution_count++;
// 可选:限制总执行次数
if (sandbox->execution_count > 1000) {
return JS_ThrowInternalError(sandbox->ctx,
"Execution limit exceeded for tenant %s",
sandbox->config.tenant_id);
}
return JS_Eval(sandbox->ctx, code, strlen(code),
"<tenant>", 0);
}
// 销毁租户沙箱
void destroy_tenant_sandbox(TenantSandbox *sandbox) {
if (sandbox) {
JS_FreeContext(sandbox->ctx);
JS_FreeRuntime(sandbox->rt);
free(sandbox);
}
}
int main() {
// 租户 A
TenantConfig config_a = {
.tenant_id = "tenant-A",
.memory_limit = 8 * 1024 * 1024, // 8MB
.timeout_seconds = 1.0
};
TenantSandbox *sandbox_a = create_tenant_sandbox(&config_a);
// 租户 B
TenantConfig config_b = {
.tenant_id = "tenant-B",
.memory_limit = 16 * 1024 * 1024, // 16MB
.timeout_seconds = 5.0
};
TenantSandbox *sandbox_b = create_tenant_sandbox(&config_b);
// 在不同租户中执行代码
const char *code_a = "let x = 42; x * 2";
const char *code_b = "let arr = [1,2,3]; arr.map(x => x*x)";
JSValue r_a = tenant_eval(sandbox_a, code_a);
if (!JS_IsException(r_a)) {
int32_t val;
JS_ToInt32(sandbox_a->ctx, &val, r_a);
printf("Tenant A result: %d\n", val);
}
JS_FreeValue(sandbox_a->ctx, r_a);
JSValue r_b = tenant_eval(sandbox_b, code_b);
if (!JS_IsException(r_b)) {
const char *json = JS_ToCString(sandbox_b->ctx,
JS_JSONStringify(sandbox_b->ctx, r_b,
JS_UNDEFINED, JS_UNDEFINED));
printf("Tenant B result: %s\n", json);
JS_FreeCString(sandbox_b->ctx, json);
}
JS_FreeValue(sandbox_b->ctx, r_b);
// 清理
destroy_tenant_sandbox(sandbox_a);
destroy_tenant_sandbox(sandbox_b);
return 0;
}
7.8 安全 API 设计
暴露受控 API 给沙箱
// safe_api.c — 向沙箱暴露受控的宿主 API
#include "quickjs-libc.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 受控的 HTTP 请求(模拟)
static JSValue api_http_get(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
const char *url = JS_ToCString(ctx, argv[0]);
// 安全检查:只允许特定域名
if (strncmp(url, "https://api.myapp.com/", 22) != 0) {
JS_FreeCString(ctx, url);
return JS_ThrowSecurityError(ctx,
"Only api.myapp.com is allowed");
}
printf("Fetching: %s\n", url);
JS_FreeCString(ctx, url);
// 模拟返回
JSValue result = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, result, "status", JS_NewInt32(ctx, 200));
JS_SetPropertyStr(ctx, result, "data",
JS_NewString(ctx, "{\"message\": \"ok\"}"));
return result;
}
// 受控的日志(输出到审计系统)
static JSValue api_log(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv) {
const char *level = JS_ToCString(ctx, argv[0]);
const char *message = JS_ToCString(ctx, argv[1]);
// 输出到审计日志
printf("[AUDIT][%s] %s\n", level, message);
JS_FreeCString(ctx, level);
JS_FreeCString(ctx, message);
return JS_UNDEFINED;
}
// 注册安全 API
void setup_safe_api(JSContext *ctx) {
JSValue global = JS_GetGlobalObject(ctx);
JSValue api = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, api, "httpGet",
JS_NewCFunction(ctx, api_http_get, "httpGet", 1));
JS_SetPropertyStr(ctx, api, "log",
JS_NewCFunction(ctx, api_log, "log", 2));
JS_SetPropertyStr(ctx, global, "api", api);
JS_FreeValue(ctx, global);
}
7.9 生产环境安全检查清单
| 检查项 | 说明 | 状态 |
|---|
移除 std 模块 | 不注册 js_init_module_std | ☐ |
移除 os 模块 | 不注册 js_init_module_os | ☐ |
移除 print | 删除全局 print 函数 | ☐ |
移除 scriptArgs | 防止参数泄露 | ☐ |
移除 __loadScript | 防止加载外部脚本 | ☐ |
| 设置内存限制 | JS_SetMemoryLimit() | ☐ |
| 设置栈大小 | JS_SetMaxStackSize() | ☐ |
| 设置超时中断 | JS_SetInterruptHandler() | ☐ |
| 禁用动态导入 | 移除 import() 支持 | ☐ |
| 输入验证 | 验证所有传入沙箱的数据 | ☐ |
| 输出检查 | 检查执行结果不包含敏感信息 | ☐ |
| 审计日志 | 记录所有沙箱执行操作 | ☐ |
7.10 本章小结
| 要点 | 说明 |
|---|
| 沙箱隔离 | 不注册 std/os 模块,移除危险全局 |
| 超时控制 | 使用 JS_SetInterruptHandler 实现 |
| 内存限制 | JS_SetMemoryLimit() + JS_SetMaxStackSize() |
| 递归保护 | QuickJS 内置栈溢出检测 |
| 操作码限制 | 通过中断回调计数 |
| 多租户 | 每租户独立 Runtime,完全隔离 |
扩展阅读