Some checks failed
CI/CD Pipeline / lint (push) Successful in 3m45s
CI/CD Pipeline / integration-tests (push) Failing after 55s
CI/CD Pipeline / unit-tests (push) Failing after 1m1s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped
255 lines
9.7 KiB
Rust
255 lines
9.7 KiB
Rust
use anyhow::Result;
|
|
use deno_core::{JsRuntime, RuntimeOptions, v8};
|
|
use serde_json::Value;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
pub struct DenoRuntime {
|
|
// We create a new runtime for each execution to ensure isolation
|
|
// In a production environment, we might want to pool runtimes or use isolates more efficiently
|
|
}
|
|
|
|
impl DenoRuntime {
|
|
pub fn new() -> Self {
|
|
Self {}
|
|
}
|
|
|
|
pub async fn execute(&self, code: String, payload: Option<Value>, headers: HashMap<String, String>) -> Result<(String, String, u16, HashMap<String, String>)> {
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
|
|
rt.block_on(async {
|
|
let result = Self::execute_inner(code, payload, headers).await;
|
|
let _ = tx.send(result);
|
|
});
|
|
});
|
|
|
|
rx.await.map_err(|_| anyhow::anyhow!("Deno execution thread panicked"))?
|
|
}
|
|
|
|
async fn execute_inner(code: String, payload: Option<Value>, headers: HashMap<String, String>) -> Result<(String, String, u16, HashMap<String, String>)> {
|
|
// Initialize JS Runtime
|
|
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
|
|
|
// 1. Inject Preamble (Polyfills for Deno.serve, Request, Response, Headers)
|
|
let preamble = r#"
|
|
globalThis.console = {
|
|
log: (...args) => {
|
|
Deno.core.print(args.map(a => String(a)).join(" ") + "\n");
|
|
},
|
|
error: (...args) => {
|
|
Deno.core.print("[ERROR] " + args.map(a => String(a)).join(" ") + "\n", true);
|
|
}
|
|
};
|
|
|
|
class Headers {
|
|
constructor(init) {
|
|
this.map = new Map();
|
|
if (init) {
|
|
if (init instanceof Headers) {
|
|
init.forEach((v, k) => this.map.set(k.toLowerCase(), v));
|
|
} else if (Array.isArray(init)) {
|
|
init.forEach(([k, v]) => this.map.set(k.toLowerCase(), v));
|
|
} else {
|
|
Object.entries(init).forEach(([k, v]) => this.map.set(k.toLowerCase(), v));
|
|
}
|
|
}
|
|
}
|
|
get(key) { return this.map.get(key.toLowerCase()) || null; }
|
|
set(key, value) { this.map.set(key.toLowerCase(), value); }
|
|
has(key) { return this.map.has(key.toLowerCase()); }
|
|
forEach(callback) { this.map.forEach(callback); }
|
|
entries() { return this.map.entries(); }
|
|
}
|
|
globalThis.Headers = Headers;
|
|
|
|
globalThis.Deno = {
|
|
serve: (handler) => {
|
|
globalThis._handler = handler;
|
|
},
|
|
core: Deno.core,
|
|
env: {
|
|
get: (key) => {
|
|
return globalThis._env ? globalThis._env[key] : null;
|
|
},
|
|
toObject: () => {
|
|
return globalThis._env || {};
|
|
}
|
|
}
|
|
};
|
|
|
|
class Response {
|
|
constructor(body, init) {
|
|
this.body = body;
|
|
this.status = init?.status || 200;
|
|
this.headers = new Headers(init?.headers);
|
|
}
|
|
async text() { return String(this.body); }
|
|
async json() { return JSON.parse(this.body); }
|
|
}
|
|
globalThis.Response = Response;
|
|
|
|
class Request {
|
|
constructor(url, init) {
|
|
this.url = url;
|
|
this.method = init?.method || "GET";
|
|
this._body = init?.body;
|
|
this.headers = new Headers(init?.headers);
|
|
}
|
|
async json() { return typeof this._body === 'string' ? JSON.parse(this._body) : this._body; }
|
|
async text() { return typeof this._body === 'string' ? this._body : JSON.stringify(this._body); }
|
|
}
|
|
globalThis.Request = Request;
|
|
"#;
|
|
|
|
runtime.execute_script("<preamble>", preamble.to_string())?;
|
|
|
|
// 2. Execute User Code
|
|
runtime.execute_script("<user_script>", code.to_string())?;
|
|
|
|
// 3. Invoke Handler
|
|
// Double-serialize to prevent JS injection: the outer JSON string is parsed
|
|
// by JSON.parse() in JS, producing the original value safely.
|
|
let payload_json = serde_json::to_string(&payload.unwrap_or(serde_json::json!({})))?;
|
|
let headers_json = serde_json::to_string(&headers)?;
|
|
let safe_payload = serde_json::to_string(&payload_json)?;
|
|
let safe_headers = serde_json::to_string(&headers_json)?;
|
|
|
|
let invoke_script = format!(r#"
|
|
(async () => {{
|
|
if (!globalThis._handler) {{
|
|
return {{ error: "No handler registered via Deno.serve" }};
|
|
}}
|
|
try {{
|
|
const headers = JSON.parse({1});
|
|
const body = JSON.parse({0});
|
|
const req = new Request("http://localhost", {{
|
|
method: "POST",
|
|
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
headers: headers
|
|
}});
|
|
const res = await globalThis._handler(req);
|
|
const text = await res.text();
|
|
|
|
const resHeaders = {{}};
|
|
if (res.headers && typeof res.headers.forEach === 'function') {{
|
|
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
}}
|
|
|
|
return {{
|
|
result: text,
|
|
headers: resHeaders,
|
|
status: res.status
|
|
}};
|
|
}} catch (e) {{
|
|
return {{ error: String(e) }};
|
|
}}
|
|
}})()
|
|
"#, safe_payload, safe_headers);
|
|
|
|
let result_val = runtime.execute_script("<invocation>", invoke_script)?;
|
|
#[allow(deprecated)]
|
|
let result = runtime.resolve_value(result_val).await?;
|
|
|
|
let scope = &mut runtime.handle_scope();
|
|
let local = v8::Local::new(scope, result);
|
|
let deserialized_value: Value = deno_core::serde_v8::from_v8(scope, local)?;
|
|
|
|
let stdout = if let Some(res) = deserialized_value.get("result") {
|
|
res.as_str().unwrap_or("").to_string()
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
let stderr = if let Some(err) = deserialized_value.get("error") {
|
|
err.as_str().unwrap_or("Unknown error").to_string()
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
let status = if let Some(s) = deserialized_value.get("status") {
|
|
s.as_u64().unwrap_or(200) as u16
|
|
} else {
|
|
200
|
|
};
|
|
|
|
let mut headers = HashMap::new();
|
|
if let Some(h) = deserialized_value.get("headers") {
|
|
if let Some(obj) = h.as_object() {
|
|
for (k, v) in obj {
|
|
if let Some(s) = v.as_str() {
|
|
headers.insert(k.clone(), s.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok((stdout, stderr, status, headers))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::{json, Value};
|
|
|
|
/// Validates that the double-serialization technique produces safe JS string
|
|
/// literals, even when the payload contains characters that could break out
|
|
/// of a JS template if interpolated naively.
|
|
#[test]
|
|
fn test_double_serialize_escapes_js_injection() {
|
|
let malicious_payload = json!({
|
|
"key": "\"); process.exit(1); //"
|
|
});
|
|
|
|
let first = serde_json::to_string(&malicious_payload).unwrap();
|
|
let double = serde_json::to_string(&first).unwrap();
|
|
|
|
// The double-serialized value must be a valid JSON string
|
|
let recovered_first: String = serde_json::from_str(&double).unwrap();
|
|
let recovered: Value = serde_json::from_str(&recovered_first).unwrap();
|
|
assert_eq!(recovered, malicious_payload);
|
|
}
|
|
|
|
#[test]
|
|
fn test_double_serialize_handles_backtick_injection() {
|
|
let payload = json!({
|
|
"attack": "${globalThis.Deno.exit()}"
|
|
});
|
|
|
|
let first = serde_json::to_string(&payload).unwrap();
|
|
let double = serde_json::to_string(&first).unwrap();
|
|
|
|
// The value when placed in a JS template literal is still just a string
|
|
let recovered_first: String = serde_json::from_str(&double).unwrap();
|
|
let recovered: Value = serde_json::from_str(&recovered_first).unwrap();
|
|
assert_eq!(recovered, payload);
|
|
}
|
|
|
|
#[test]
|
|
fn test_double_serialize_handles_empty() {
|
|
let payload = json!({});
|
|
let first = serde_json::to_string(&payload).unwrap();
|
|
let double = serde_json::to_string(&first).unwrap();
|
|
|
|
let recovered_first: String = serde_json::from_str(&double).unwrap();
|
|
let recovered: Value = serde_json::from_str(&recovered_first).unwrap();
|
|
assert_eq!(recovered, payload);
|
|
}
|
|
|
|
#[test]
|
|
fn test_double_serialize_preserves_unicode() {
|
|
let payload = json!({"emoji": "🔐", "chinese": "安全"});
|
|
let first = serde_json::to_string(&payload).unwrap();
|
|
let double = serde_json::to_string(&first).unwrap();
|
|
|
|
let recovered_first: String = serde_json::from_str(&double).unwrap();
|
|
let recovered: Value = serde_json::from_str(&recovered_first).unwrap();
|
|
assert_eq!(recovered, payload);
|
|
}
|
|
}
|