强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

QuickJS 嵌入式 JavaScript 引擎完全教程 / 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,完全隔离

扩展阅读