diff --git a/Cargo.lock b/Cargo.lock index 4ad7f35..2159d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,6 +102,17 @@ dependencies = [ "syn", ] +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -709,6 +720,16 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -893,6 +914,29 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -959,17 +1003,6 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" -[[package]] -name = "pmutil" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.78" @@ -1063,6 +1096,7 @@ name = "redos-cli" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "clap", "fancy-regex", "flate2", @@ -1077,6 +1111,7 @@ dependencies = [ "swc_ecma_visit", "tar", "tempdir", + "tokio", ] [[package]] @@ -1228,6 +1263,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "2.9.2" @@ -1294,6 +1335,15 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -1387,9 +1437,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "0.33.15" +version = "0.33.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3792c10fa5d3e93a705b31f13fdea4a6e68c3c20d4351e84ed1741b7864399cd" +checksum = "cc30ce6695b841f0a9ae01a9ca10ac3922cff559a6253c756a203c4332c62945" dependencies = [ "ast_node", "atty", @@ -1414,9 +1464,9 @@ dependencies = [ [[package]] name = "swc_ecma_ast" -version = "0.111.1" +version = "0.112.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12b4d0f3b31d293dac16fc13a50f8a282a3bdb658f2a000ffe09b1b638f45c9" +checksum = "032f528398358da8ff2fe795755602b4a81ffc93430b9830c0e1d5f198d8f48d" dependencies = [ "bitflags 2.4.2", "is-macro", @@ -1431,9 +1481,9 @@ dependencies = [ [[package]] name = "swc_ecma_parser" -version = "0.142.1" +version = "0.143.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3eedda441af51ca25caebb88837649a40e2a39b763344a53cfedd869740c71" +checksum = "3f311ee5fafd37ece487a0a21b6c8fb4f1a35ea6f9de6e76bccabc6f0db46d9f" dependencies = [ "either", "new_debug_unreachable", @@ -1453,9 +1503,9 @@ dependencies = [ [[package]] name = "swc_ecma_visit" -version = "0.97.1" +version = "0.98.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ecefeec816318f1d449b4bac2e28a4243a167cc16620e15c3c1f2d91085770" +checksum = "889fc0ec3a9b55377e53e3d4ce06678247b635d7136c1e5d3a2c26578e16cd22" dependencies = [ "num-bigint", "swc_atoms", @@ -1489,9 +1539,9 @@ dependencies = [ [[package]] name = "swc_visit" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27078d8571abe23aa52ef608dd1df89096a37d867cf691cbb4f4c392322b7c9" +checksum = "3f5b3e8d1269a7cb95358fed3412645d9c15aa0eb1f4ca003a25a38ef2f30f1b" dependencies = [ "either", "swc_visit_macros", @@ -1499,12 +1549,11 @@ dependencies = [ [[package]] name = "swc_visit_macros" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8bb05975506741555ea4d10c3a3bdb0e2357cd58e1a4a4332b8ebb4b44c34d" +checksum = "33fc817055fe127b4285dc85058596768bfde7537ae37da82c67815557f03e33" dependencies = [ "Inflector", - "pmutil", "proc-macro2", "quote", "swc_macros_common", @@ -1603,20 +1652,34 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/crates/redos-cli/Cargo.toml b/crates/redos-cli/Cargo.toml index abd5e1d..c2201d4 100644 --- a/crates/redos-cli/Cargo.toml +++ b/crates/redos-cli/Cargo.toml @@ -18,8 +18,10 @@ owo-colors = "4.0" redos = { path = "../redos" } reqwest = { version = "0.11.20", features = ["blocking"] } swc_common = { version = "0.33", features = ["tty-emitter"] } -swc_ecma_ast = "0.111" -swc_ecma_parser = { version = "0.142.0", features = ["typescript"] } -swc_ecma_visit = "0.97" +swc_ecma_ast = "0.112" +swc_ecma_parser = { version = "0.143", features = ["typescript"] } +swc_ecma_visit = "0.98" tar = "0.4.40" tempdir = "0.3.7" +tokio = { version = "1.36.0", features = ["full"] } +async-trait = "0.1.77" diff --git a/crates/redos-cli/src/languages/javascript.rs b/crates/redos-cli/src/languages/javascript.rs new file mode 100644 index 0000000..350ac7c --- /dev/null +++ b/crates/redos-cli/src/languages/javascript.rs @@ -0,0 +1,101 @@ +use std::path::Path; + +use swc_common::sync::Lrc; +use swc_common::{ + errors::{ColorConfig, Handler}, + SourceMap, +}; +use swc_ecma_ast::{EsVersion, Regex}; +use swc_ecma_parser::TsConfig; +use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; +use swc_ecma_visit::{fold_module_item, Fold}; + +use anyhow::{anyhow, Result}; + +use async_trait::async_trait; + +use super::language::{Language, Location}; + +/// List of scanned extensions +const EXTENSIONS: [&str; 8] = ["js", "jsx", "ts", "tsx", "mjs", "cjs", "mts", "cts"]; + +pub struct JavaScript; + +#[async_trait(?Send)] +impl Language for JavaScript { + async fn check_file(path: &Path) -> Result>> { + let ext = path.extension().unwrap_or_default(); + + if !EXTENSIONS.contains(&ext.to_str().unwrap()) { + return Ok(None); + } + + let cm: Lrc = Default::default(); + let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); + + let fm = cm.load_file(path)?; + + let lexer = Lexer::new( + Syntax::Typescript(TsConfig { + tsx: true, + decorators: true, + dts: false, + no_early_errors: true, + disallow_ambiguous_jsx_like: false, + }), + EsVersion::latest(), + StringInput::from(&*fm), + None, + ); + + let mut parser = Parser::new_from(lexer); + + let module = parser + .parse_module() + .map_err(|e| { + // Unrecoverable fatal error occurred + e.into_diagnostic(&handler).emit(); + }) + .map_err(|_| anyhow!("Failed to parse file"))?; + + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + + for token in module.body { + let tx = tx.clone(); + fold_module_item( + &mut Visitor { + callback: Box::new(move |regex| { + // regex_list.push(regex.to_string()); + tx.blocking_send(regex).unwrap(); + }), + }, + token, + ); + } + + let mut regex_list = vec![]; + + while let Some(regex) = rx.recv().await { + regex_list.push(regex); + } + + Ok(Some(regex_list)) + } +} + +struct Visitor { + callback: Box, +} + +impl Fold for Visitor { + fn fold_regex(&mut self, regex: Regex) -> Regex { + (self.callback)(( + regex.exp.as_ref().to_string(), + Location { + line: regex.span.lo.0 as usize, + column: regex.span.hi.0 as usize, + }, + )); + regex + } +} diff --git a/crates/redos-cli/src/languages/language.rs b/crates/redos-cli/src/languages/language.rs new file mode 100644 index 0000000..0cd8f95 --- /dev/null +++ b/crates/redos-cli/src/languages/language.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use async_trait::async_trait; +use std::fmt::Display; +use std::path::Path; + +#[derive(Debug)] +pub struct Location { + pub line: usize, + pub column: usize, +} + +impl Display for Location { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.line, self.column) + } +} + +#[async_trait(?Send)] +pub trait Language { + /// Scans a file for every known regex. + /// Returns None if the file is not supported. + /// + /// Else, returns a list of regexes and their location in the file. + async fn check_file(path: &Path) -> Result>>; +} diff --git a/crates/redos-cli/src/languages/mod.rs b/crates/redos-cli/src/languages/mod.rs new file mode 100644 index 0000000..5f81f12 --- /dev/null +++ b/crates/redos-cli/src/languages/mod.rs @@ -0,0 +1,2 @@ +pub mod javascript; +pub mod language; diff --git a/crates/redos-cli/src/main.rs b/crates/redos-cli/src/main.rs index f3f5e50..a913ad5 100644 --- a/crates/redos-cli/src/main.rs +++ b/crates/redos-cli/src/main.rs @@ -1,25 +1,22 @@ +mod languages; mod repo; -use core::panic; use std::path::{Path, PathBuf}; use clap::{Parser as ClapParser, Subcommand}; use fancy_regex::parse::Parser as FancyParser; use ignore::WalkBuilder; +use languages::{ + javascript::JavaScript, + language::{Language, Location}, +}; use owo_colors::OwoColorize; use redos::vulnerabilities; use repo::parse_repository; -use swc_common::sync::Lrc; -use swc_common::{ - errors::{ColorConfig, Handler}, - SourceMap, -}; -use swc_ecma_ast::{EsVersion, Regex}; -use swc_ecma_parser::TsConfig; -use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; -use swc_ecma_visit::{fold_module_item, Fold}; use tempdir::TempDir; +use anyhow::Result; + use crate::repo::download_repository; #[derive(ClapParser)] @@ -71,10 +68,16 @@ enum ScanCommand { }, } -/// List of scanned extensions -const EXTENSIONS: [&str; 8] = ["js", "jsx", "ts", "tsx", "mjs", "cjs", "mts", "cts"]; +fn print_regex(regex: &str, location: Location, raw: bool, path: &Path) { + if raw { + println!("{}", regex); + } else { + println!("{}:{}", path.to_str().unwrap(), location); + println!(" {}", regex.red()); + } +} -fn local_scan(all: bool, raw: bool, directory: Option) { +async fn local_scan(all: bool, raw: bool, directory: Option) -> Result<()> { let walk = WalkBuilder::new(directory.unwrap_or_else(|| ".".into())).build(); for entry in walk { @@ -83,16 +86,27 @@ fn local_scan(all: bool, raw: bool, directory: Option) { if entry.file_type().unwrap().is_file() { let path = entry.path(); - if let Some(extension) = path.extension() { - if EXTENSIONS.contains(&extension.to_str().unwrap()) { - check_file(path, raw, all); + let regexes = JavaScript::check_file(path).await?; + + if let Some(regexes) = regexes { + for regex in regexes { + if all + || !vulnerabilities(®ex.0, &Default::default())? + .vulnerabilities + .is_empty() + { + print_regex(®ex.0, regex.1, raw, path); + } } } } } + + Ok(()) } -fn main() { +#[tokio::main] +async fn main() -> Result<()> { let args = Cli::parse(); match args.command { Commands::Scan { command } => match command { @@ -103,7 +117,7 @@ fn main() { exclude: _, raw, } => { - local_scan(all, raw, directory); + local_scan(all, raw, directory).await?; } ScanCommand::Git { repository, @@ -118,78 +132,13 @@ fn main() { directory.path().to_str().unwrap() ); - local_scan(all, raw, Some(directory.into_path())); + local_scan(all, raw, Some(directory.into_path())).await?; } }, Commands::Ast { regex } => { println!("{:#?}", FancyParser::parse(regex.as_str())); } } -} - -fn check_file(path: &Path, raw: bool, show_all: bool) { - let cm: Lrc = Default::default(); - let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); - - let fm = cm - .load_file(path) - .unwrap_or_else(|e| panic!("failed to load file: {}", e)); - - let lexer = Lexer::new( - Syntax::Typescript(TsConfig { - tsx: true, - decorators: true, - dts: false, - no_early_errors: true, - disallow_ambiguous_jsx_like: false, - }), - EsVersion::latest(), - StringInput::from(&*fm), - None, - ); - - let mut parser = Parser::new_from(lexer); - - let module = parser.parse_module().map_err(|e| { - // Unrecoverable fatal error occurred - e.into_diagnostic(&handler).emit(); - }); - - if let Ok(module) = module { - for token in module.body { - fold_module_item( - &mut Visitor { - show_all, - raw, - path: path.into(), - }, - token, - ); - } - } -} - -struct Visitor { - show_all: bool, - path: PathBuf, - raw: bool, -} -impl Fold for Visitor { - fn fold_regex(&mut self, regex: Regex) -> Regex { - if self.show_all - || !vulnerabilities(regex.exp.as_ref(), &Default::default()) - .unwrap() - .vulnerabilities - .is_empty() - { - if self.raw { - println!("{}", regex.exp); - } else { - println!("{}:{}", self.path.to_str().unwrap(), regex.span.lo.0); - println!(" {}", regex.exp.red()); - } - } - regex - } + Ok(()) } diff --git a/crates/redos-wasm/src/lib.rs b/crates/redos-wasm/src/lib.rs index b408469..94180c4 100644 --- a/crates/redos-wasm/src/lib.rs +++ b/crates/redos-wasm/src/lib.rs @@ -11,7 +11,7 @@ pub fn ir(regex: &str) -> String { let parser = Parser::parse(regex); format!( "{:#?}", - parser.map(|tree| redos::ir::to_expr(&tree, &tree.expr, &Default::default())) + parser.map(|tree| redos::ir::to_expr(&tree.expr, &Default::default())) ) } diff --git a/crates/redos/src/ir.rs b/crates/redos/src/ir.rs index 8cbfa74..1822a8e 100644 --- a/crates/redos/src/ir.rs +++ b/crates/redos/src/ir.rs @@ -1,7 +1,7 @@ //! Immediate representation of a regular expression. //! Used to simplify the AST and make it easier to work with. -use fancy_regex::{parse::ExprTree, Assertion, Expr as RegexExpr, LookAround}; +use fancy_regex::{Assertion, Expr as RegexExpr, LookAround}; use crate::vulnerability::VulnerabilityConfig; @@ -54,7 +54,7 @@ pub enum Expr { } /// Converts a fancy-regex AST to an IR AST -pub fn to_expr(tree: &ExprTree, expr: &RegexExpr, config: &VulnerabilityConfig) -> Option { +pub fn to_expr(expr: &RegexExpr, config: &VulnerabilityConfig) -> Option { match expr { RegexExpr::Empty => None, RegexExpr::Any { .. } => Some(Expr::Token), @@ -62,19 +62,17 @@ pub fn to_expr(tree: &ExprTree, expr: &RegexExpr, config: &VulnerabilityConfig) RegexExpr::Literal { .. } => Some(Expr::Token), RegexExpr::Concat(list) => Some(Expr::Concat( list.iter() - .map(|e| to_expr(tree, e, config)) - .filter_map(|e| e) + .filter_map(|e| to_expr(e, config)) .collect(), )), RegexExpr::Alt(list) => Some(Expr::Alt( list.iter() - .map(|e| to_expr(tree, e, config)) - .filter_map(|e| e) + .filter_map(|e| to_expr(e, config)) .collect(), )), - RegexExpr::Group(e) => to_expr(tree, e, config).map(|e| Expr::Group(Box::new(e))), + RegexExpr::Group(e) => to_expr(e, config).map(|e| Expr::Group(Box::new(e))), RegexExpr::LookAround(e, la) => { - to_expr(tree, e, config).map(|e| Expr::LookAround(Box::new(e), *la)) + to_expr(e, config).map(|e| Expr::LookAround(Box::new(e), *la)) } RegexExpr::Repeat { child, @@ -85,12 +83,12 @@ pub fn to_expr(tree: &ExprTree, expr: &RegexExpr, config: &VulnerabilityConfig) let range = hi - lo; let expression = if range > config.max_quantifier { - to_expr(tree, child, config).map(|child| Expr::Repeat { + to_expr(child, config).map(|child| Expr::Repeat { child: Box::new(child), greedy: *greedy, }) } else { - to_expr(tree, child, config) + to_expr(child, config) }; if *lo == 0 { @@ -106,7 +104,7 @@ pub fn to_expr(tree: &ExprTree, expr: &RegexExpr, config: &VulnerabilityConfig) // false negatives RegexExpr::Backref(_) => Some(Expr::Token), RegexExpr::AtomicGroup(e) => { - to_expr(tree, e, config).map(|e| Expr::AtomicGroup(Box::new(e))) + to_expr(e, config).map(|e| Expr::AtomicGroup(Box::new(e))) } RegexExpr::KeepOut => None, RegexExpr::ContinueFromPreviousMatchEnd => None, @@ -116,14 +114,14 @@ pub fn to_expr(tree: &ExprTree, expr: &RegexExpr, config: &VulnerabilityConfig) true_branch, false_branch, } => { - let true_branch = to_expr(tree, true_branch, config); - let false_branch = to_expr(tree, false_branch, config); + let true_branch = to_expr(true_branch, config); + let false_branch = to_expr(false_branch, config); if let (Some(true_branch), Some(false_branch)) = (true_branch, false_branch) { let condition: Option = match condition.as_ref() { &RegexExpr::BackrefExistsCondition(number) => Some(ExprConditional::BackrefExistsCondition(number)), - expr => to_expr(tree, expr, config).map(|x| ExprConditional::Condition(Box::new(x))) + expr => to_expr(expr, config).map(|x| ExprConditional::Condition(Box::new(x))) }; condition.map(|condition| Expr::Conditional { diff --git a/crates/redos/src/lib.rs b/crates/redos/src/lib.rs index 8a9c475..3ae8692 100644 --- a/crates/redos/src/lib.rs +++ b/crates/redos/src/lib.rs @@ -14,7 +14,7 @@ use vulnerability::{Vulnerability, VulnerabilityConfig}; /// - It must contain a repeat /// - The repeat must have a bound size greater than `config.max_quantifier` /// - The regex must have a terminating state (to allow for backtracking) (TODO: this is not implemented yet) -fn repeats_anywhere(expr: &Expr, config: &VulnerabilityConfig) -> bool { +fn repeats_anywhere(expr: &Expr) -> bool { match expr { Expr::Repeat { .. } => true, @@ -23,12 +23,12 @@ fn repeats_anywhere(expr: &Expr, config: &VulnerabilityConfig) -> bool { Expr::Assertion(_) => false, // propagate - Expr::Concat(list) => list.iter().any(|e| repeats_anywhere(e, config)), - Expr::Alt(list) => list.iter().any(|e| repeats_anywhere(e, config)), - Expr::Group(e) => repeats_anywhere(e.as_ref(), config), - Expr::LookAround(e, _) => repeats_anywhere(e.as_ref(), config), - Expr::AtomicGroup(e) => repeats_anywhere(e.as_ref(), config), - Expr::Optional(e) => repeats_anywhere(e.as_ref(), config), + Expr::Concat(list) => list.iter().any(repeats_anywhere), + Expr::Alt(list) => list.iter().any(repeats_anywhere), + Expr::Group(e) => repeats_anywhere(e.as_ref()), + Expr::LookAround(e, _) => repeats_anywhere(e.as_ref()), + Expr::AtomicGroup(e) => repeats_anywhere(e.as_ref()), + Expr::Optional(e) => repeats_anywhere(e.as_ref()), Expr::Conditional { condition, true_branch, @@ -36,9 +36,9 @@ fn repeats_anywhere(expr: &Expr, config: &VulnerabilityConfig) -> bool { } => { match condition { ExprConditional::BackrefExistsCondition(_) => false, - ExprConditional::Condition(condition) => repeats_anywhere(condition.as_ref(), config) - || repeats_anywhere(true_branch.as_ref(), config) - || repeats_anywhere(false_branch.as_ref(), config) + ExprConditional::Condition(condition) => repeats_anywhere(condition.as_ref()) + || repeats_anywhere(true_branch.as_ref()) + || repeats_anywhere(false_branch.as_ref()) } } } @@ -71,10 +71,10 @@ pub fn vulnerabilities(regex: &str, config: &VulnerabilityConfig) -> Result