use oxc::ast::ast::Expression;
const SAFE_OBJECTS: &[&str] = &["Math", "JSON", "Number", "String", "Array", "Object"];
const SAFE_FUNCTIONS: &[&str] = &[
"parseInt", "parseFloat", "isNaN", "isFinite",
"encodeURI", "decodeURI", "encodeURIComponent", "decodeURIComponent",
"atob", "btoa", "escape", "unescape",
"Number", "String", "Boolean",
];
const UNSAFE_GLOBALS: &[&str] = &[
"eval", "Function", "require", "import", "process",
"console", "fetch", "XMLHttpRequest", "WebSocket",
"setTimeout", "setInterval", "setImmediate",
"global", "globalThis", "window", "document", "navigator",
"fs", "child_process", "net", "http", "https",
];
pub fn is_safe_expr(expr: &Expression) -> bool {
match expr {
Expression::NumericLiteral(_)
| Expression::StringLiteral(_)
| Expression::BooleanLiteral(_)
| Expression::NullLiteral(_) => true,
Expression::Identifier(id) => {
let name = id.name.as_str();
!UNSAFE_GLOBALS.contains(&name)
}
Expression::BinaryExpression(b) => is_safe_expr(&b.left) && is_safe_expr(&b.right),
Expression::UnaryExpression(u) => is_safe_expr(&u.argument),
Expression::LogicalExpression(l) => is_safe_expr(&l.left) && is_safe_expr(&l.right),
Expression::ConditionalExpression(c) => {
is_safe_expr(&c.test) && is_safe_expr(&c.consequent) && is_safe_expr(&c.alternate)
}
Expression::CallExpression(call) => {
if !call.arguments.iter().all(|a| a.as_expression().is_some_and(is_safe_expr)) {
return false;
}
match &call.callee {
Expression::Identifier(id) => SAFE_FUNCTIONS.contains(&id.name.as_str()),
Expression::StaticMemberExpression(m) => {
matches!(&m.object, Expression::Identifier(id) if SAFE_OBJECTS.contains(&id.name.as_str()))
|| matches!(&m.object, Expression::StringLiteral(_))
}
_ => false,
}
}
Expression::StaticMemberExpression(m) => is_safe_expr(&m.object),
Expression::ComputedMemberExpression(m) => is_safe_expr(&m.object) && is_safe_expr(&m.expression),
Expression::ParenthesizedExpression(p) => is_safe_expr(&p.expression),
Expression::ArrayExpression(arr) => arr.elements.iter().all(|el| {
el.as_expression().is_some_and(is_safe_expr)
}),
Expression::TemplateLiteral(t) => t.expressions.iter().all(is_safe_expr),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxc::allocator::Allocator;
use oxc::parser::Parser;
use oxc::span::SourceType;
fn parse_expr<'a>(alloc: &'a Allocator, src: &'a str) -> Expression<'a> {
Parser::new(alloc, src, SourceType::mjs()).parse_expression().unwrap()
}
#[test]
fn test_safe_literals() {
let a = Allocator::default();
assert!(is_safe_expr(&parse_expr(&a, "42")));
assert!(is_safe_expr(&parse_expr(&a, "\"hello\"")));
}
#[test]
fn test_safe_math() {
let a = Allocator::default();
assert!(is_safe_expr(&parse_expr(&a, "Math.floor(1.7)")));
assert!(is_safe_expr(&parse_expr(&a, "parseInt(\"42\")")));
}
#[test]
fn test_unsafe_eval() {
let a = Allocator::default();
assert!(!is_safe_expr(&parse_expr(&a, "eval(\"code\")")));
}
#[test]
fn test_unsafe_process() {
let a = Allocator::default();
assert!(!is_safe_expr(&parse_expr(&a, "process.exit()")));
}
#[test]
fn test_unsafe_require() {
let a = Allocator::default();
assert!(!is_safe_expr(&parse_expr(&a, "require(\"fs\")")));
}
}