>(&self, spec: P) -> bool;
69 | }
70 |
71 | // Implement a best-effort Node.js-compatible module path determination,
72 | // that takes into account both CommonJS and ESM.
73 | // See https://nodejs.org/api/packages.html#determining-module-system
74 | pub(crate) fn resolve_path(base_path: P, exists_predicate: F) -> Option
75 | where
76 | P: AsRef,
77 | F: Fn(&Path) -> bool,
78 | {
79 | let mut path = base_path.as_ref().to_path_buf();
80 |
81 | // Check if path itself exists.
82 | if exists_predicate(&path) {
83 | return Some(path);
84 | }
85 |
86 | // Try path combined with JavaScript suffixes.
87 | for suffix in ["js", "mjs", "cjs"] {
88 | path.set_extension(suffix);
89 | if exists_predicate(&path) {
90 | return Some(path);
91 | }
92 | }
93 |
94 | // Try entrypoints in the path's directory.
95 | for entrypoint in ["index.js", "index.mjs", "index.cjs"] {
96 | path = base_path.as_ref().to_path_buf().join(entrypoint);
97 | if exists_predicate(&path) {
98 | return Some(path);
99 | }
100 | }
101 |
102 | None
103 | }
104 |
105 | // For an import specifier to be recognized as relative in Javascript, it _has_
106 | // to start with either "." or "..". This means that we can use this predicate
107 | // to avoid trying to resolve as relative a spec which is bare or absolute.
108 | //
109 | // https://nodejs.org/api/modules.html#all-together
110 | // https://nodejs.org/api/esm.html#import-specifiers
111 | fn is_relative>(path: P) -> bool {
112 | let path = path.as_ref();
113 | path.starts_with(".") || path.starts_with("..")
114 | }
115 |
116 | fn is_valid_js_extension>(path: P) -> bool {
117 | path.as_ref().extension().map_or(false, |ext| {
118 | let lowercase_ext = ext.to_string_lossy().to_lowercase();
119 | ["js", "mjs", "cjs"].contains(&&*lowercase_ext)
120 | })
121 | }
122 |
123 | #[derive(Deserialize)]
124 | struct PackageJson {
125 | main: Option,
126 | }
127 |
128 | // Retrieve the entry point from the "main" field in `package.json`.
129 | pub(crate) fn entry_point(package_json: &str) -> Result {
130 | serde_json::from_str::(package_json)
131 | .map_err(|e| Error::Generic(format!("package.json deserialization error: {e:?}")))
132 | .map(|p| p.main.unwrap_or_else(|| PathBuf::from("index.js")))
133 | .map(|path| util::normalize_path(&path))
134 | }
135 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/module/resolver/tgz.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::io::Read;
3 | use std::path::{Path, PathBuf};
4 |
5 | use flate2::read::GzDecoder;
6 | use tar::Archive;
7 |
8 | use super::{entry_point, is_valid_js_extension};
9 | use crate::javascript::module::resolver::ModuleResolver;
10 | use crate::javascript::module::Module;
11 | use crate::{Error, Result, Tree};
12 |
13 | pub struct TarballModuleResolver {
14 | files: HashMap>,
15 | entry_point: PathBuf,
16 | }
17 |
18 | impl TarballModuleResolver {
19 | pub fn new(bytes: Vec) -> Result {
20 | // Immediately extract the file in-memory because random access directly
21 | // from the tarball object is not possible.
22 | let files = Archive::new(GzDecoder::new(&bytes[..]))
23 | .entries()?
24 | .filter_map(|entry| entry.ok())
25 | .filter_map(|mut entry| {
26 | // Skip the toplevel directory, to go to the module's root.
27 | let mut path = PathBuf::from(entry.path().unwrap());
28 | path = path.components().skip(1).collect();
29 |
30 | let mut data = Vec::new();
31 | entry.read_to_end(&mut data).ok()?;
32 |
33 | Some((path, data))
34 | })
35 | .collect::>();
36 |
37 | let package_json = std::str::from_utf8(files.get(Path::new("package.json")).unwrap())
38 | .map_err(|e| Error::Generic(format!("package.json: {}", e)))?;
39 |
40 | let entry_point = entry_point(package_json)?;
41 |
42 | Ok(Self { files, entry_point })
43 | }
44 | }
45 |
46 | impl ModuleResolver for TarballModuleResolver {
47 | fn entry_point(&self) -> &str {
48 | // Unwrap is always valid because the entry point comes from a
49 | // `package.json` file which should be UTF-8 to begin with.
50 | self.entry_point.to_str().unwrap()
51 | }
52 |
53 | fn load>(&self, path: P) -> Result {
54 | let path = self.resolve_absolute(path)?;
55 |
56 | let source = std::str::from_utf8(self.files.get(&path).unwrap())
57 | .map_err(|e| Error::Generic(format!("{}: {}", path.display(), e)))?
58 | .to_string();
59 | let tree = Tree::new(source)?.try_into()?;
60 |
61 | Ok(tree)
62 | }
63 |
64 | fn all_paths(&self) -> Box + '_> {
65 | let paths = self.files.keys().filter(|path| is_valid_js_extension(path)).cloned();
66 | Box::new(paths)
67 | }
68 |
69 | fn exists>(&self, path: P) -> bool {
70 | self.files.contains_key(path.as_ref())
71 | }
72 |
73 | fn is_dir>(&self, _spec: P) -> bool {
74 | // Paths in a tarball are never directories.
75 | false
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/package/mod.rs:
--------------------------------------------------------------------------------
1 | //! Package-level (i.e. Javascript files pertaining to a `package.json`) data
2 | //! structures and methods.
3 | pub mod reachability;
4 | pub mod resolver;
5 |
6 | use std::collections::HashMap;
7 | use std::fs;
8 | use std::path::{Path, PathBuf};
9 |
10 | use reachability::{PackageReachability, VulnerableNode};
11 |
12 | use super::lang::imports::{CommonJsImports, EsmImports};
13 | use crate::javascript::lang::imports::Imports;
14 | use crate::javascript::module::{
15 | FilesystemModuleResolver, MemModuleResolver, Module, ModuleCache, ModuleResolver,
16 | TarballModuleResolver,
17 | };
18 | use crate::Result;
19 |
20 | /// A Javascript package.
21 | pub struct Package {
22 | cache: ModuleCache,
23 | resolver: R,
24 | }
25 |
26 | impl Package
27 | where
28 | R: ModuleResolver,
29 | {
30 | pub fn resolve_relative, Q: AsRef>(
31 | &self,
32 | spec: P,
33 | base: Q,
34 | ) -> Option<&Module> {
35 | self.resolver
36 | .resolve_relative(spec.as_ref(), base.as_ref())
37 | .ok()
38 | .and_then(|path| self.cache.module(path))
39 | }
40 |
41 | pub fn resolve_absolute>(&self, spec: P) -> Option<&Module> {
42 | self.resolver.resolve_absolute(spec.as_ref()).ok().and_then(|path| self.cache.module(path))
43 | }
44 |
45 | pub fn cache(&self) -> &ModuleCache {
46 | &self.cache
47 | }
48 |
49 | pub fn resolver(&self) -> &R {
50 | &self.resolver
51 | }
52 |
53 | pub fn reachability(&self, vulnerable_node: &VulnerableNode) -> Result {
54 | PackageReachability::new(self, vulnerable_node)
55 | }
56 |
57 | /// Identify foreign imports for each module.
58 | ///
59 | /// A foreign import is an import from a source that's outside this package.
60 | /// For convenience, we are going to mark all imports that _don't_ resolve
61 | /// inside the package as foreign; true unreachable exports will be just
62 | /// dropped.
63 | pub fn foreign_imports(&self) -> HashMap<&PathBuf, Imports> {
64 | let mut foreign_imports = HashMap::new();
65 |
66 | // Strategy for detecting foreign imports: discard trivially relative
67 | // paths (i.e. starting with '.'), then attempt to resolve paths from
68 | // inside the package, marking the import as foreign if that fails.
69 | for (spec, module) in self.cache().modules() {
70 | let import_filter = |import: &str| {
71 | !import.starts_with('.') && self.resolve_relative(import, spec).is_none()
72 | };
73 |
74 | let import_specs = match module.imports() {
75 | Imports::Esm(imports) => Imports::Esm(EsmImports::new(
76 | imports
77 | .into_iter()
78 | .filter(|import| import_filter(import.source()))
79 | .cloned()
80 | .collect(),
81 | )),
82 | Imports::CommonJs(imports) => Imports::CommonJs(CommonJsImports::new(
83 | imports.into_iter().filter(|&import| import_filter(import.source())).collect(),
84 | )),
85 | Imports::None => continue,
86 | };
87 | foreign_imports.insert(spec, import_specs);
88 | }
89 |
90 | foreign_imports
91 | }
92 | }
93 |
94 | impl Package {
95 | pub fn from_tarball_path>(tarball: P) -> Result {
96 | fs::read(tarball.as_ref()).map_err(|e| e.into()).and_then(Package::from_tarball_bytes)
97 | }
98 |
99 | pub fn from_tarball_bytes(bytes: Vec) -> Result {
100 | let resolver = TarballModuleResolver::new(bytes)?;
101 | let cache = ModuleCache::new(&resolver)?;
102 | Ok(Self { resolver, cache })
103 | }
104 | }
105 |
106 | impl Package {
107 | pub fn from_path>(path: P) -> Result {
108 | let resolver = FilesystemModuleResolver::new(path.as_ref())?;
109 | let cache = ModuleCache::new(&resolver)?;
110 | Ok(Self { resolver, cache })
111 | }
112 | }
113 |
114 | impl Package {
115 | pub fn from_mem(backing: HashMap) -> Result
116 | where
117 | K: Into,
118 | V: Into,
119 | {
120 | let resolver = MemModuleResolver::new(backing);
121 | let cache = ModuleCache::new(&resolver)?;
122 | Ok(Self { resolver, cache })
123 | }
124 | }
125 |
126 | #[cfg(test)]
127 | mod tests {
128 | use maplit::hashmap;
129 | use textwrap::dedent;
130 |
131 | use super::*;
132 |
133 | fn fixture() -> Package {
134 | Package::from_mem(hashmap! {
135 | "package.json" => r#"{ "main": "index.js" }"#.to_string(),
136 | "index.js" => dedent(r#"
137 | import foo from '@foo/bar'
138 | import bar from 'foobar'
139 | import baz from './baz.js'
140 | import quux from './quux.js'
141 | "#),
142 | "baz.js" => dedent(r#"
143 | export default const c = 1
144 | "#)
145 | })
146 | .unwrap()
147 | }
148 |
149 | #[test]
150 | fn test_foreign_imports() {
151 | let fixture = fixture();
152 | let foreign_imports = fixture.foreign_imports();
153 |
154 | fn vec_of_imports<'a>(imports: &'a Imports) -> Vec<&'a str> {
155 | match imports {
156 | Imports::Esm(imports) => imports.into_iter().map(|v| v.source()).collect(),
157 | _ => unreachable!(),
158 | }
159 | }
160 |
161 | println!("{:#?}", foreign_imports);
162 | assert_eq!(
163 | foreign_imports
164 | .iter()
165 | .map(|(&k, v)| (k.clone(), vec_of_imports(v)))
166 | .collect::>(),
167 | hashmap! {
168 | PathBuf::from("index.js") => vec!["@foo/bar", "foobar"]
169 | }
170 | );
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/package/resolver.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::path::{Component, Path};
3 |
4 | use crate::javascript::module::{Module, ModuleResolver};
5 | use crate::javascript::package::Package;
6 |
7 | /// A cache of packages.
8 | pub struct PackageResolver {
9 | packages: HashMap>,
10 | }
11 |
12 | impl PackageResolver
13 | where
14 | R: ModuleResolver,
15 | {
16 | /// Creates a new builder.
17 | pub fn builder() -> PackageResolverBuilder {
18 | Default::default()
19 | }
20 |
21 | /// Returns the specified package, if present.
22 | pub fn resolve_package(&self, package: &str) -> Option<&Package> {
23 | self.packages.get(package)
24 | }
25 |
26 | /// Resolves a package and a module given a full import spec.
27 | ///
28 | /// # Examples
29 | ///
30 | /// ```js
31 | /// import "@foo/bar/baz/quux.js" // yields ("@foo/bar", "baz/quux.js")
32 | /// import "foo/bar/baz.js" // yields ("foo", "bar/baz.js")
33 | /// ```
34 | pub fn resolve_spec(&self, full_spec: &Path) -> Option<(&Package, &Module)> {
35 | let (pkg_path, spec) = match full_spec.components().next() {
36 | Some(Component::Normal(x)) if x.to_string_lossy().starts_with('@') => {
37 | let mut c = full_spec.components();
38 | let scope = c.next()?;
39 | let package = c.next()?;
40 | let spec = c.as_path();
41 |
42 | let scope_path: &Path = scope.as_ref();
43 | let package_path: &Path = package.as_ref();
44 |
45 | Some((scope_path.to_path_buf().join(package_path), spec))
46 | },
47 | Some(Component::Normal(_)) => {
48 | let mut c = full_spec.components();
49 | let package = c.next()?;
50 | let spec = c.as_path();
51 |
52 | let package_path: &Path = package.as_ref();
53 |
54 | Some((package_path.to_path_buf(), spec))
55 | },
56 | _ => None,
57 | }?;
58 |
59 | let package = self.resolve_package(&pkg_path.to_string_lossy())?;
60 | let module = package.resolve_absolute(spec)?;
61 |
62 | Some((package, module))
63 | }
64 |
65 | pub fn package_specs(&self) -> impl Iterator- {
66 | self.packages.keys()
67 | }
68 | }
69 |
70 | /// Builder for [`PackageResolver`].
71 | pub struct PackageResolverBuilder {
72 | packages: HashMap>,
73 | }
74 |
75 | impl Default for PackageResolverBuilder
76 | where
77 | R: ModuleResolver,
78 | {
79 | fn default() -> Self {
80 | Self { packages: Default::default() }
81 | }
82 | }
83 |
84 | impl PackageResolverBuilder
85 | where
86 | R: ModuleResolver,
87 | {
88 | /// Inserts the specified package in the resolver.
89 | pub fn with_package>(mut self, k: S, v: Package) -> Self {
90 | self.packages.insert(k.into(), v);
91 | self
92 | }
93 |
94 | /// Builds the value.
95 | pub fn build(self) -> PackageResolver {
96 | PackageResolver { packages: self.packages }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/project/mod.rs:
--------------------------------------------------------------------------------
1 | //! Project-wide (i.e. tree-of-packages) reachability.
2 |
3 | use std::collections::{HashMap, HashSet, VecDeque};
4 | use std::hash::Hash;
5 |
6 | use serde::{Deserialize, Serialize};
7 | use tree_sitter::Node;
8 |
9 | use super::lang::imports::Imports;
10 | use super::package::reachability::{PackageReachability, VulnerableNode};
11 | use super::package::Package;
12 | use crate::javascript::module::resolver::ModuleResolver;
13 | use crate::javascript::package::reachability::NodePath;
14 | use crate::javascript::package::resolver::PackageResolver;
15 | use crate::{Error, Result};
16 |
17 | /// Specifies the package and module that direct towards a vulnerability.
18 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
19 | pub struct VulnerableEdge {
20 | package: String,
21 | module: String,
22 | }
23 |
24 | impl VulnerableEdge {
25 | pub fn new, S2: Into>(package: S1, module: S2) -> Self {
26 | Self { package: package.into(), module: module.into() }
27 | }
28 | }
29 |
30 | /// An instance of in-package reachability that leads either to
31 | /// the vulnerability itself, or to an intermediate package.
32 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
33 | pub enum ReachabilityEdge {
34 | /// The vulnerability itself is reachable.
35 | Own(PackageReachability),
36 | /// The vulnerability is reachable through another package in the dependency
37 | /// tree.
38 | Foreign(PackageReachability, VulnerableEdge),
39 | }
40 |
41 | impl ReachabilityEdge {
42 | fn reachability(&self) -> &PackageReachability {
43 | match self {
44 | ReachabilityEdge::Own(p) => p,
45 | ReachabilityEdge::Foreign(p, _) => p,
46 | }
47 | }
48 | }
49 |
50 | /// A single path between a node in a package and a vulnerable node.
51 | ///
52 | /// This is a hierarchical ordered representation, of the following form:
53 | ///
54 | /// ```json
55 | /// {
56 | /// "dependent_package": {
57 | /// "module1.js": ["node1", "node2", ...]
58 | /// },
59 | /// ...,
60 | /// "vulnerable_package": {
61 | /// "module2.js": ["node3", "node4", ... "vulnerable_node"]
62 | /// }
63 | /// }
64 | /// ```
65 | pub type ReachabilityPath = Vec<(String, Vec<(String, NodePath)>)>;
66 |
67 | /// Reachability of a given node inside of a project.
68 | ///
69 | /// Maps a package name to the graph of accesses (represented as adjacency
70 | /// lists) that lead to the vulnerable node, starting from that package.
71 | #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
72 | pub struct ProjectReachability(HashMap>);
73 |
74 | impl ProjectReachability {
75 | pub fn new(adjacency_lists: HashMap>) -> Self {
76 | Self(adjacency_lists)
77 | }
78 |
79 | /// Return the graph edges leaving from the specified package, if they
80 | /// exist.
81 | pub fn edges_from(&self, package: &str) -> Option<&Vec> {
82 | self.0.get(package)
83 | }
84 |
85 | /// Find one possible path from the specified package to the vulnerable
86 | /// node.
87 | pub fn find_path>(&self, start_package: S) -> Option {
88 | struct EvaluationStep<'a> {
89 | src_package: &'a str,
90 | src_module: &'a str,
91 | step_path: Vec<(&'a str, Vec<(String, NodePath)>)>,
92 | }
93 | let mut bfs_q = VecDeque::new();
94 | let mut visited = HashSet::new();
95 |
96 | // Initialize the queue with steps for all modules in the top level package.
97 | //
98 | // This maps to the client having N files in their project, and starting a
99 | // search for a path from each of those N files.
100 | for edge in self.0.get(start_package.as_ref())? {
101 | for module_spec in edge.reachability().modules() {
102 | bfs_q.push_back(EvaluationStep {
103 | src_package: start_package.as_ref(),
104 | src_module: module_spec.as_ref(),
105 | step_path: Vec::new(),
106 | });
107 | }
108 | }
109 |
110 | while let Some(EvaluationStep { src_package, src_module, step_path }) = bfs_q.pop_front() {
111 | // Skip already visited packages (would be a circular dependency).
112 | if visited.contains(src_package) {
113 | continue;
114 | }
115 | visited.insert(src_package);
116 |
117 | // Stop searching in this branch if it would lead to a non-existing package.
118 | // In practice, this should only happen in the top-level package if it is
119 | // misnamed, as `self` should contain only valid edges.
120 | let Some(edges) = self.0.get(src_package) else { continue };
121 |
122 | for edge in edges {
123 | // Each edge maintains its own copy of the step path.
124 | let mut step_path = step_path.clone();
125 |
126 | // If a subpath forward is found, add it to the path. Otherwise,
127 | // skip processing this edge.
128 | if let Some(path) = edge.reachability().find_path(src_module) {
129 | step_path.push((src_package, path));
130 | } else {
131 | continue;
132 | }
133 |
134 | match edge {
135 | ReachabilityEdge::Own(_) => {
136 | // This is the last branch. Return the found path.
137 | return Some(
138 | step_path.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
139 | );
140 | },
141 | ReachabilityEdge::Foreign(_, symbol) => {
142 | // This is an intermediate branch. Add a step towards the next symbol.
143 | bfs_q.push_back(EvaluationStep {
144 | src_package: &symbol.package,
145 | src_module: &symbol.module,
146 | step_path: step_path.clone(),
147 | });
148 | },
149 | }
150 | }
151 | }
152 |
153 | None
154 | }
155 | }
156 |
157 | pub struct Project {
158 | package_resolver: PackageResolver,
159 | }
160 |
161 | impl Project {
162 | pub fn new(package_resolver: PackageResolver) -> Self {
163 | Self { package_resolver }
164 | }
165 |
166 | pub fn reachability(&self, vuln_node: &VulnerableNode) -> Result {
167 | self.reachability_inner(vuln_node, Default::default())
168 | }
169 |
170 | pub fn reachability_extend(
171 | &self,
172 | project_reachability: ProjectReachability,
173 | vuln_node: &VulnerableNode,
174 | ) -> Result {
175 | self.reachability_inner(vuln_node, project_reachability)
176 | }
177 |
178 | pub fn all_packages(&self) -> Vec<(&str, &Package)> {
179 | self.package_resolver
180 | .package_specs()
181 | .filter_map(|package_spec| {
182 | self.package_resolver
183 | .resolve_package(package_spec)
184 | .map(|package| (package_spec.as_str(), package))
185 | })
186 | .collect()
187 | }
188 |
189 | fn reachability_inner<'a>(
190 | &'a self,
191 | vuln_node: &'a VulnerableNode,
192 | package_reachabilities: ProjectReachability,
193 | ) -> Result {
194 | let ProjectReachability(mut package_reachabilities) = package_reachabilities;
195 |
196 | // Using foreign imports for each package, build a list of edges (a, b)
197 | // where b depends on a.
198 | let mut edges = HashSet::<(&str, &str)>::new();
199 |
200 | let all_foreign_imports: HashMap<_, _> = self
201 | .all_packages()
202 | .into_iter()
203 | .map(|(package_spec, package)| (package_spec, (package, package.foreign_imports())))
204 | .collect();
205 |
206 | for (package_spec, (_package, foreign_imports)) in &all_foreign_imports {
207 | for dependencies in foreign_imports.values() {
208 | let insert_edge = |(dependency, _)| {
209 | edges.insert((package_spec, dependency));
210 | };
211 |
212 | match &dependencies {
213 | Imports::Esm(imports) => imports
214 | .into_iter()
215 | .map(|import| import.source())
216 | .map(package_spec_split)
217 | .for_each(insert_edge),
218 | Imports::CommonJs(imports) => imports
219 | .into_iter()
220 | .map(|import| import.source())
221 | .map(package_spec_split)
222 | .for_each(insert_edge),
223 | Imports::None => continue,
224 | };
225 | }
226 | }
227 |
228 | let edges = edges.into_iter().collect::>();
229 |
230 | // Find the topological sorting of this graph so that we can find the
231 | // reachability for the leaves first and for all the dependents afterwards.
232 | let topo_ordering = topological_sort(&edges)?;
233 |
234 | // For each dependent A on dependency B, compute reachability between A's
235 | // imports and B's exports.
236 | //
237 | // Let's say that the first package with a vulnerability comes Kth in the list.
238 | // - Packages 1 to K-1 will have empty reachability data by construction; they
239 | // do not depend on (any of) the vulnerable package(s).
240 | // - Package K will have the reachability information starting from the given
241 | // vulnerable node(s).
242 | // - Packages K+1 to N will be able to construct their reachability information
243 | // starting from the information in package K's reachability.
244 |
245 | for package_spec in topo_ordering.into_iter().rev() {
246 | if package_reachabilities.contains_key(package_spec) {
247 | continue;
248 | }
249 |
250 | let mut target_nodes: Vec<(VulnerableNode, Option)> = Vec::new();
251 |
252 | // If the vulnerable spec pertains to the currently processed package,
253 | // add the node to the list of target nodes
254 | if package_spec == vuln_node.package() {
255 | target_nodes.push((vuln_node.clone(), None));
256 | }
257 |
258 | // A package in topo_ordering must always be present in the resolver
259 | // and it should also have an entry in all_foreign_imports. Skip
260 | // (but report) missing specs as they might be system packages
261 | // (e.g `fs`, `events`).
262 | let Some((package, foreign_imports)) = all_foreign_imports.get(package_spec) else {
263 | eprintln!("Package spec not found in project: {package_spec}");
264 | continue;
265 | };
266 |
267 | // Identify target nodes coming from foreign imports. This will not
268 | // run for the leaves, i.e. packages with no dependencies.
269 | for (module_spec, specs) in foreign_imports {
270 | // Link foreign imports to the symbol exported from the foreign package.
271 | let mut link_exports = |name: Option<&str>, source: &str, node: Node<'_>| {
272 | // Extract package name, module name and reachability struct for the
273 | // imported foreign package.
274 | let (dep_package, dep_module) = package_spec_split(source);
275 | let dep_module = dep_module
276 | .or_else(|| {
277 | let package = self.package_resolver.resolve_package(dep_package)?;
278 | Some(package.resolver().entry_point())
279 | })
280 | .unwrap_or("index.js");
281 | let Some(dep_reachabilities) = package_reachabilities.get(dep_package) else {
282 | return;
283 | };
284 |
285 | let reachable_exports = dep_reachabilities
286 | .iter()
287 | .map(|edge| edge.reachability().reachable_exports())
288 | .fold(HashSet::new(), |mut set, exports| {
289 | set.extend(exports);
290 | set
291 | });
292 |
293 | let reachability_check = match name {
294 | // The hardcoded "" check is for CommonJs modules, and it represents
295 | // exports that are a function. Unconditionally, any access to exports at
296 | // all that are in this form is to be considered
297 | // reaching.
298 | //
299 | // This part in CommonJs is currently shortcircuited by the `None` branch.
300 | Some(name) => {
301 | reachable_exports.contains(&(dep_module, name))
302 | || reachable_exports.contains(&(dep_module, ""))
303 | },
304 | // On this branch, all exports match, regardless of their symbol.
305 | // This is an over-coloring that is only useful with CommonJs until
306 | // a better way of evaluating objects is found.
307 | None => reachable_exports.iter().any(|&(module, _)| module == dep_module),
308 | };
309 |
310 | // If there is a reachability match between imports and
311 | // foreign exports, insert the current import node in the
312 | // target nodes, and attach information about the imported
313 | // symbol.
314 | if reachability_check {
315 | let start_pos = node.start_position();
316 | let end_pos = node.end_position();
317 | target_nodes.push((
318 | VulnerableNode::new(
319 | package_spec,
320 | module_spec.to_string_lossy(),
321 | start_pos.row,
322 | start_pos.column,
323 | end_pos.row,
324 | end_pos.column,
325 | ),
326 | Some(VulnerableEdge::new(dep_package, dep_module)),
327 | ));
328 | }
329 | };
330 |
331 | match &specs {
332 | Imports::Esm(imports) => {
333 | for import in imports {
334 | link_exports(Some(import.name()), import.source(), import.node());
335 | }
336 | },
337 | Imports::CommonJs(imports) => {
338 | let module = package.cache().module(module_spec).unwrap();
339 | let tree = module.tree();
340 |
341 | for import in imports {
342 | let access_node = import.access_node(tree);
343 | link_exports(None, import.source(), access_node);
344 | }
345 | },
346 | Imports::None => {},
347 | }
348 | }
349 |
350 | // Compute the reachability from each target node to the visible
351 | // exports/side effects in a package.
352 | for (reachability, foreign) in target_nodes
353 | .into_iter()
354 | .map(|(target_node, foreign)| (package.reachability(&target_node), foreign))
355 | {
356 | match reachability {
357 | Ok(reachability) => {
358 | if !reachability.is_empty() {
359 | let reachability_edge = match foreign {
360 | Some(foreign) => ReachabilityEdge::Foreign(reachability, foreign),
361 | None => ReachabilityEdge::Own(reachability),
362 | };
363 |
364 | package_reachabilities
365 | .entry(package_spec.to_string())
366 | .or_default()
367 | .push(reachability_edge);
368 | }
369 | },
370 | Err(e) => eprintln!("Reachability failed: {e:?}"),
371 | }
372 | }
373 | }
374 |
375 | // Pick out reachability for the topmost package. By construction, it should
376 | // always be the last one in the topological ordering. We should build tests
377 | // to ensure this is the case.
378 | Ok(ProjectReachability::new(package_reachabilities))
379 | }
380 | }
381 |
382 | // Utilities
383 | //
384 |
385 | // Split a package spec into package name and module spec.
386 | //
387 | // # Example
388 | //
389 | // ```rust
390 | // assert_eq!(package_spec_split("@foo/bar"), ("@foo/bar", None));
391 | // assert_eq!(package_spec_split("@foo/bar/baz.js"), ("@foo/bar", Some("baz.js")));
392 | // assert_eq!(package_spec_split("foo"), ("foo", None));
393 | // assert_eq!(package_spec_split("foo/bar.js"), ("foo", Some("bar.js")));
394 | // ```
395 | fn package_spec_split(s: &str) -> (&str, Option<&str>) {
396 | let idx = if s.starts_with('@') { 1 } else { 0 };
397 |
398 | s.match_indices('/')
399 | .nth(idx)
400 | .map(|(index, _)| s.split_at(index))
401 | .map(|(package, module)| (package, Some(&module[1..])))
402 | .unwrap_or_else(|| (s, None))
403 | }
404 |
405 | // Implementation of https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
406 | fn topological_sort(edges: &[(N, N)]) -> Result> {
407 | let mut nodes = {
408 | // Build a set of all vertices.
409 | let all_nodes = edges
410 | .iter()
411 | .copied()
412 | .flat_map(|(dependent, dependency)| [dependent, dependency])
413 | .collect::>();
414 |
415 | // Build a set of vertices with at least one incoming edge.
416 | let nodes_with_inc_edges =
417 | edges.iter().copied().map(|(_, dependency)| dependency).collect::>();
418 |
419 | // Build the set of vertices with no incoming edge by difference between the two
420 | // above. This is also going to be the working set of the algorithm.
421 | all_nodes.difference(&nodes_with_inc_edges).copied().collect::>()
422 | };
423 |
424 | // Build reversed adjacency lists tp gradually remove edges from.
425 | let mut adj_lists: HashMap> =
426 | edges.iter().copied().fold(HashMap::new(), |mut adj_lists, (dependent, dependency)| {
427 | adj_lists.entry(dependency).or_default().insert(dependent);
428 | adj_lists
429 | });
430 |
431 | let mut ordering = Vec::new();
432 |
433 | while !nodes.is_empty() {
434 | let n = nodes.pop_front().unwrap();
435 | ordering.push(n);
436 |
437 | for (dependency, dependents) in &mut adj_lists {
438 | if dependents.remove(&n) && dependents.is_empty() {
439 | nodes.push_back(*dependency);
440 | }
441 | }
442 | }
443 |
444 | // Input graph should be a DAG hence not have any cycles; it means that
445 | // all the edges have been processed.
446 | if adj_lists.iter().any(|(_, s)| !s.is_empty()) {
447 | return Err(Error::Generic(
448 | "Could not find topological ordering: cycles detected in the dependency tree".into(),
449 | ));
450 | }
451 |
452 | Ok(ordering)
453 | }
454 |
455 | #[cfg(test)]
456 | mod tests {
457 | use maplit::hashmap;
458 | use textwrap::dedent;
459 |
460 | use super::*;
461 | use crate::javascript::module::MemModuleResolver;
462 | use crate::javascript::package::Package;
463 |
464 | macro_rules! mem_fixture {
465 | ($($module:expr => $src:expr,)*) => {
466 | Package::from_mem(hashmap! {
467 | $($module => dedent($src)),*
468 | }).unwrap()
469 | }
470 | }
471 |
472 | fn project_trivial_esm() -> Project {
473 | let package_resolver = PackageResolver::builder()
474 | .with_package(
475 | "dependency",
476 | mem_fixture!(
477 | "package.json" => r#"{ "main": "index.js" }"#,
478 | "index.js" => r#"
479 | import { vuln } from './vuln.js'
480 |
481 | export function vuln2() { vuln() }
482 | "#,
483 | "vuln.js" => r#"
484 | export function vuln() { const foo = 2 }
485 | "#,
486 | ),
487 | )
488 | .with_package(
489 | "dependent",
490 | mem_fixture!(
491 | "package.json" => r#"{ "main": "index.js" }"#,
492 | "index.js" => r#"
493 | import { vuln2 } from 'dependency'
494 |
495 | export function dependent_vuln() { vuln2() }
496 | "#,
497 | ),
498 | )
499 | .build();
500 |
501 | Project { package_resolver }
502 | }
503 |
504 | fn project_trivial_cjs() -> Project {
505 | let package_resolver = PackageResolver::builder()
506 | .with_package(
507 | "dependency",
508 | mem_fixture!(
509 | "package.json" => r#"{ "main": "index.js" }"#,
510 | "index.js" => r#"
511 | const vuln = require('./vuln.js')
512 |
513 | module.exports.vuln2 = function() { vuln.vuln() }
514 | "#,
515 | "vuln.js" => r#"
516 | module.exports.vuln = function() { const foo = 2 }
517 | "#,
518 | ),
519 | )
520 | .with_package(
521 | "dependent",
522 | mem_fixture!(
523 | "package.json" => r#"{ "main": "index.js" }"#,
524 | "index.js" => r#"
525 | const dependency = require('dependency')
526 |
527 | module.exports.vuln3 = function() { dependency.vuln2() }
528 | "#,
529 | ),
530 | )
531 | .build();
532 |
533 | Project { package_resolver }
534 | }
535 |
536 | #[test]
537 | fn test_topo_sort() {
538 | let edges =
539 | &[(5, 11), (7, 11), (7, 8), (3, 8), (3, 10), (11, 2), (11, 9), (11, 10), (8, 9)];
540 |
541 | let t = topological_sort(edges).unwrap();
542 |
543 | let is_before =
544 | |a, b| t.iter().position(|i| i == a).unwrap() < t.iter().position(|i| i == b).unwrap();
545 |
546 | // Every dependent must come before all of its dependencies.
547 | for (a, b) in edges {
548 | assert!(is_before(a, b), "{a} -> {b}");
549 | }
550 | }
551 |
552 | #[test]
553 | fn test_package_spec_split() {
554 | assert_eq!(package_spec_split("@foo/bar"), ("@foo/bar", None));
555 | assert_eq!(package_spec_split("@foo/bar/baz.js"), ("@foo/bar", Some("baz.js")));
556 | assert_eq!(package_spec_split("foo"), ("foo", None));
557 | assert_eq!(package_spec_split("foo/bar.js"), ("foo", Some("bar.js")));
558 | }
559 |
560 | #[test]
561 | fn test_trivial_reachability_esm() {
562 | let project = project_trivial_esm();
563 |
564 | let r = project
565 | .reachability_inner(
566 | &VulnerableNode::new("dependency", "vuln.js", 1, 31, 1, 34),
567 | Default::default(),
568 | )
569 | .unwrap();
570 | let path = r.find_path("dependent").unwrap();
571 | println!("{:#?}", r);
572 | print_path(path);
573 | }
574 |
575 | #[test]
576 | fn test_trivial_reachability_cjs() {
577 | let project = project_trivial_cjs();
578 |
579 | let r = project
580 | .reachability_inner(
581 | &VulnerableNode::new("dependency", "vuln.js", 1, 41, 1, 44),
582 | Default::default(),
583 | )
584 | .unwrap();
585 | let path = r.find_path("dependent").unwrap();
586 | println!("{:#?}", r);
587 | print_path(path);
588 | }
589 |
590 | fn print_path(package_path: Vec<(String, Vec<(String, NodePath)>)>) {
591 | for (package, module_path) in package_path {
592 | println!("\x1b[34;1m{package}\x1b[0m:");
593 | for (module, node_path) in module_path {
594 | println!(" \x1b[36;1m{module}\x1b[0m:");
595 | for node_step in node_path {
596 | let (r, c) = node_step.start();
597 | println!(" {:>4}:{:<5} {}", r, c, node_step.symbol(),);
598 | }
599 | }
600 | }
601 | }
602 | }
603 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/queries/commonjs-exports.lsp:
--------------------------------------------------------------------------------
1 | ; The order of the patterns must be preserved, or maintained alongside the
2 | ; users of this query. Unfortunately there is no efficient way of identifying
3 | ; a query pattern except its index.
4 |
5 | ; Pattern #0
6 | ; module.exports = identifier produces an unnamed identifier
7 | (
8 | (assignment_expression
9 | left: (member_expression
10 | object: (identifier) @cjs-module
11 | property: (property_identifier) @cjs-exports)
12 | right: (identifier) @target-ident)
13 | (#eq? @cjs-module "module")
14 | (#eq? @cjs-exports "exports")
15 | )
16 |
17 | ; Pattern #1
18 | ; module.exports = function () {} produces an unnamed scope
19 | (
20 | (assignment_expression
21 | left: (member_expression
22 | object: (identifier) @cjs-module
23 | property: (property_identifier) @cjs-exports)
24 | right: (_ body: (_) @target-scope))
25 | (#eq? @cjs-module "module")
26 | (#eq? @cjs-exports "exports")
27 | )
28 |
29 | ; Pattern #2
30 | ; module.exports = { a, b, c } produces [a, b, c]
31 | (
32 | (assignment_expression
33 | left: (member_expression
34 | object: (identifier) @cjs-module
35 | property: (property_identifier) @cjs-exports)
36 | right: (object) @target-object)
37 | (#eq? @cjs-module "module")
38 | (#eq? @cjs-exports "exports")
39 | )
40 |
41 | ; Pattern #3
42 | ; module.exports.foo = ... produces [foo]
43 | (
44 | (assignment_expression
45 | left: (member_expression
46 | object: (member_expression
47 | object: (identifier) @cjs-module
48 | property: (property_identifier) @cjs-exports)
49 | property: (property_identifier) @target-name)
50 | right: (_) @target-object)
51 | (#eq? @cjs-module "module")
52 | (#eq? @cjs-exports "exports")
53 | )
54 |
55 | ; Pattern #4
56 | ; exports.foo = ... produces [foo] (rare, but happens)
57 | (
58 | (assignment_expression
59 | left: (member_expression
60 | object: (identifier) @cjs-exports
61 | property: (property_identifier) @target-name)
62 | right: (_) @target-object)
63 | (#eq? @cjs-exports "exports")
64 | )
65 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/queries/commonjs-imports.lsp:
--------------------------------------------------------------------------------
1 | (
2 | (call_expression
3 | function: (identifier) @id-require
4 | arguments: (arguments (string (string_fragment) @import-source)))
5 | (#eq? @id-require "require")
6 | )
7 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/queries/esm-exports.lsp:
--------------------------------------------------------------------------------
1 | ; The order of the patterns must be preserved, or maintained alongside the
2 | ; users of this query. Unfortunately there is no efficient way of identifying
3 | ; a query pattern except its index.
4 |
5 | ; Pattern #0
6 | ;
7 | ; export let name = 1
8 | ; export const name = 1
9 | (export_statement
10 | !source
11 | declaration: [
12 | (lexical_declaration (variable_declarator name: (identifier) @export-decl))
13 | (variable_declaration (variable_declarator name: (identifier) @export-decl))
14 | ])
15 |
16 | ; Pattern #1
17 | ;
18 | ; export function name() {}
19 | ; export function* name() {}
20 | ; export class ClassName {}
21 | (export_statement
22 | !source
23 | declaration: [
24 | (_
25 | name: (identifier) @export-decl
26 | body: (statement_block) @export-scope)
27 | (class_declaration
28 | name: (identifier) @export-decl
29 | body: (class_body) @export-scope)
30 | ])
31 |
32 | ; Pattern #2
33 | ; export_specifier has a `name` field and, optionally, an `alias` field.
34 | ;
35 | ; export { foo, bar, baz }
36 | (export_statement
37 | !source
38 | (export_clause (export_specifier) @export-list-spec))
39 |
40 | ; Pattern #3
41 | ;
42 | ; export default function name() {}
43 | ; export default function* name() {}
44 | ; export default class ClassName {}
45 | ; export default function () {}
46 | ; export default function* () {}
47 | ; export default class {}
48 | (export_statement
49 | !source
50 | value: [
51 | (function body: (statement_block) @export-scope)
52 | (generator_function body: (statement_block) @export-scope)
53 | (class body: (class_body) @export-scope)
54 | ])
55 |
56 | ; Pattern #4
57 | ;
58 | ; export default identifier
59 | (export_statement
60 | !source
61 | value: (identifier) @export-name)
62 |
63 | ; Pattern #5
64 | ;
65 | ; export const { foo, bar } = baz
66 | ; export let { foo, bar } = baz
67 | ; export var { foo, bar } = baz
68 | ;
69 | ; Object pattern is a special case because export names are linked to the
70 | ; pattern's RHS rather than getting picked up straight from the scope in which
71 | ; they are defined.
72 | (export_statement
73 | !source
74 | declaration: (_
75 | (variable_declarator
76 | name: (object_pattern [
77 | (shorthand_property_identifier_pattern) @export-pattern
78 | (pair_pattern key: (_) @export-pattern)])
79 | value: (_) @export-source)))
80 |
81 | ; Pattern #6
82 | ;
83 | ; export const [foo, bar] = baz
84 | ; export let [foo, bar] = baz
85 | ; export var [foo, bar] = baz
86 | ;
87 | ; Same as above
88 | (export_statement
89 | !source
90 | declaration: (_
91 | (variable_declarator
92 | name: (array_pattern (_) @export-pattern)
93 | value: (_) @export-source)))
94 |
95 | ; Pattern #7
96 | ;
97 | ; export default { a: 1, b: 2 }
98 | ;
99 | ; Same as above
100 | (export_statement
101 | !source
102 | value: (_) @export-value)
103 |
104 | ; Pattern #8
105 | ;
106 | ; export * from 'source'
107 | ; export { a as b, c } from 'source'
108 | (export_statement
109 | (export_clause)? @export_clause
110 | source: (string (string_fragment) @export-source))
111 |
--------------------------------------------------------------------------------
/vuln-reach/src/javascript/queries/esm-imports.lsp:
--------------------------------------------------------------------------------
1 | ; The order of the patterns must be preserved, or maintained alongside the
2 | ; users of this query. Unfortunately there is no efficient way of identifying
3 | ; a query pattern except its index.
4 |
5 | ; Pattern #0
6 | ;
7 | ; import name from "module"
8 | (import_statement
9 | (import_clause (identifier) @import-default)
10 | source: (string (string_fragment) @import-source))
11 |
12 | ; Pattern #1
13 | ;
14 | ; import * as name from "module"
15 | (import_statement
16 | (import_clause (namespace_import (identifier) @import-star))
17 | source: (string (string_fragment) @import-source))
18 |
19 | ; Pattern #2
20 | ;
21 | ; import { foo } from "module"
22 | ; import { foo as bar } from "module"
23 | (import_statement
24 | (import_clause (named_imports (import_specifier) @import-spec))
25 | source: (string (string_fragment) @import-source))
26 |
--------------------------------------------------------------------------------
/vuln-reach/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::io;
3 | use std::ops::{Deref, DerefMut};
4 |
5 | use lazy_static::lazy_static;
6 | use thiserror::Error;
7 | use tree_sitter::{Language, LanguageError, Node, Parser, Query, QueryError, Tree as TsTree};
8 |
9 | pub mod javascript;
10 | pub mod util;
11 |
12 | pub use tree_sitter;
13 |
14 | extern "C" {
15 | fn tree_sitter_javascript() -> Language;
16 | }
17 |
18 | lazy_static! {
19 | pub static ref JS: Language = unsafe { tree_sitter_javascript() };
20 | }
21 |
22 | pub fn js_parser() -> Parser {
23 | let mut p = Parser::new();
24 | p.set_language(*JS).unwrap();
25 | p
26 | }
27 |
28 | #[derive(Error, Debug)]
29 | pub enum Error {
30 | #[error("language error: {0}")]
31 | LanguageError(#[from] LanguageError),
32 | #[error("query error: {0}")]
33 | QueryError(#[from] QueryError),
34 | #[error("i/o error: {0}")]
35 | IoError(#[from] io::Error),
36 | #[error("{0}")]
37 | Generic(String),
38 | #[error("tree contains parse errors")]
39 | ParseError,
40 | #[error("node does not exist in tree")]
41 | InvalidNode,
42 | }
43 |
44 | impl From for Error {
45 | fn from(value: String) -> Self {
46 | Error::Generic(value)
47 | }
48 | }
49 |
50 | pub type Result = std::result::Result;
51 |
52 | #[derive(Debug)]
53 | pub struct Tree {
54 | buf: String,
55 | lang: Language,
56 | tree: TsTree,
57 | }
58 |
59 | impl Tree {
60 | pub fn new(buf: String) -> Result {
61 | let mut parser = js_parser();
62 | parser.set_language(*JS)?;
63 |
64 | Self::with_parser(&mut parser, buf)
65 | }
66 |
67 | pub fn with_parser(parser: &mut Parser, buf: String) -> Result {
68 | let lang = parser
69 | .language()
70 | .ok_or_else(|| Error::Generic("Language for parser not specified".to_string()))?;
71 | let tree = parser
72 | .parse(&buf, None)
73 | .ok_or_else(|| Error::Generic("Could not parse source".to_string()))?;
74 |
75 | Ok(Tree { buf, lang, tree })
76 | }
77 |
78 | pub fn buf(&self) -> &str {
79 | &self.buf
80 | }
81 |
82 | pub fn query(&self, text: &str) -> Result {
83 | Ok(Query::new(self.lang, text)?)
84 | }
85 |
86 | pub fn repr_of(&self, node: Node) -> &str {
87 | &self.buf[node.start_byte()..node.end_byte()]
88 | }
89 | }
90 |
91 | impl Deref for Tree {
92 | type Target = TsTree;
93 |
94 | fn deref(&self) -> &Self::Target {
95 | &self.tree
96 | }
97 | }
98 |
99 | impl DerefMut for Tree {
100 | fn deref_mut(&mut self) -> &mut Self::Target {
101 | &mut self.tree
102 | }
103 | }
104 |
105 | /// Cache for fast duplication of previously used cursors.
106 | pub struct TreeCursorCache<'a> {
107 | cursors: HashMap, Cursor<'a>>,
108 | tree: &'a Tree,
109 | }
110 |
111 | impl<'a> TreeCursorCache<'a> {
112 | fn new(tree: &'a Tree) -> Self {
113 | Self { tree, cursors: HashMap::new() }
114 | }
115 |
116 | fn cursor(&mut self, node: Node<'a>) -> Result> {
117 | match self.cursors.get(&node) {
118 | Some(cursor) => Ok(cursor.clone()),
119 | None => {
120 | let cursor = Cursor::new(self.tree, node)?;
121 | self.cursors.insert(node, cursor.clone());
122 | Ok(cursor)
123 | },
124 | }
125 | }
126 | }
127 |
128 | /// Cursor for upwards traversal of a [`treesitter::Tree`].
129 | #[derive(Clone)]
130 | pub struct Cursor<'a> {
131 | nodes: Vec>,
132 | }
133 |
134 | impl<'a> Cursor<'a> {
135 | /// Construct a new cursor and move it to the `node`.
136 | pub fn new(tree: &'a Tree, node: Node<'a>) -> Result {
137 | let mut nodes = Vec::new();
138 |
139 | // Start cursor at the root, so we know all parents.
140 | let mut cursor = tree.root_node().walk();
141 | nodes.push(cursor.node());
142 |
143 | // Iterate through children until we've found the desired node.
144 | let node_end = node.end_byte();
145 | while node != nodes[nodes.len() - 1] {
146 | let child_offset = cursor.goto_first_child_for_byte(node_end);
147 |
148 | // Child does not exist in the tree.
149 | if child_offset.is_none() {
150 | return Err(Error::InvalidNode);
151 | }
152 |
153 | nodes.push(cursor.node());
154 | }
155 |
156 | Ok(Self { nodes })
157 | }
158 |
159 | /// Move the cursor to the parent node.
160 | pub fn goto_parent(&mut self) -> Option> {
161 | (self.nodes.len() > 1).then(|| {
162 | self.nodes.pop();
163 | self.node()
164 | })
165 | }
166 |
167 | /// Get the cursor's current node.
168 | pub fn node(&self) -> Node<'a> {
169 | self.nodes[self.nodes.len() - 1]
170 | }
171 |
172 | /// Get the node's parent.
173 | pub fn parent(&mut self) -> Option> {
174 | (self.nodes.len() > 1).then(|| self.nodes[self.nodes.len() - 2])
175 | }
176 |
177 | /// Get an iterator from the cursor's current node to the tree root.
178 | pub fn parents(self) -> ParentIterator<'a> {
179 | ParentIterator { cursor: self }
180 | }
181 | }
182 |
183 | pub struct ParentIterator<'a> {
184 | cursor: Cursor<'a>,
185 | }
186 |
187 | impl<'a> Iterator for ParentIterator<'a> {
188 | type Item = Node<'a>;
189 |
190 | fn next(&mut self) -> Option {
191 | self.cursor.goto_parent()
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/vuln-reach/src/util.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Component, Path, PathBuf};
2 |
3 | /// Normalize a path, removing things like `.` and `..`.
4 | ///
5 | /// CAUTION: This does not resolve symlinks (unlike
6 | /// [`std::fs::canonicalize`]). This may cause incorrect or surprising
7 | /// behavior at times. This should be used carefully. Unfortunately,
8 | /// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
9 | /// fail, or on Windows returns annoying device paths. This is a problem Cargo
10 | /// needs to improve on.
11 | ///
12 | /// Taken from .
13 | pub fn normalize_path(path: &Path) -> PathBuf {
14 | let mut components = path.components().peekable();
15 | let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
16 | components.next();
17 | PathBuf::from(c.as_os_str())
18 | } else {
19 | PathBuf::new()
20 | };
21 |
22 | for component in components {
23 | match component {
24 | Component::Prefix(..) => unreachable!(),
25 | Component::RootDir => {
26 | ret.push(component.as_os_str());
27 | },
28 | Component::CurDir => {},
29 | Component::ParentDir => {
30 | ret.pop();
31 | },
32 | Component::Normal(c) => {
33 | ret.push(c);
34 | },
35 | }
36 | }
37 | ret
38 | }
39 |
40 | #[test]
41 | fn test_normalize_path() {
42 | // Check that traversal is prevented.
43 | assert_eq!(normalize_path(Path::new("./foo/bar")), PathBuf::from("foo/bar"));
44 | assert_eq!(normalize_path(Path::new("../foo/bar")), PathBuf::from("foo/bar"));
45 | assert_eq!(normalize_path(Path::new("../foo/../bar")), PathBuf::from("bar"));
46 | }
47 |
--------------------------------------------------------------------------------