From 6e81882680f5f533dcfe4231e74e28468959ac62 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:24:35 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20SSRF=20in=20web=5Ffetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: mudcube <101564+mudcube@users.noreply.github.com> --- .jules/sentinel.md | 5 + .../src/system_tools_plugin.rs | 187 +++++++++++++++++- web/pnpm-lock.yaml | 44 +++++ 3 files changed, 229 insertions(+), 7 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 798ec7de6..dbb009e84 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** The `OperationQueue` worker in `mill-server` executed file operations (create, write, delete, rename) using raw paths from the operation object without validating they were within the project root. **Learning:** Background workers that process serialized operations are a common bypass for security checks enforced at the API layer. The API layer might validate the request, but if the worker is "dumb" and blindly executes the queued operation, an internal attacker or a buggy component can exploit it. **Prevention:** Validation must happen at the *execution point* (in the worker), not just at the ingestion point. We introduced `validate_path` in the worker loop to enforce project root containment using `canonicalize` (handling non-existent files correctly). + +## 2025-06-03 - SSRF Vulnerability in Web Fetch Tool +**Vulnerability:** The `web_fetch` tool in `mill-plugin-system` used `reqwest::blocking::get` to fetch arbitrary user-provided URLs without validating the resolved IP addresses, allowing Server-Side Request Forgery (SSRF) against internal services. +**Learning:** Checking the URL string is not enough to prevent SSRF. Attackers can use DNS Rebinding (TOCTOU) to point a seemingly benign domain to an internal IP after the initial validation. Furthermore, if a domain resolves to multiple IPs, an attacker can mix safe and malicious IPs. +**Prevention:** We must manually resolve the domain to its IPs using `std::net::ToSocketAddrs`, validate that *all* returned IPs are allowed (not local/private subnets), and then pin the connection to one of the validated IPs using `reqwest::blocking::ClientBuilder::resolve()`. We must also disable automatic redirects and manually track them to apply this same validation to each redirect destination, up to a maximum limit. diff --git a/crates/mill-plugin-system/src/system_tools_plugin.rs b/crates/mill-plugin-system/src/system_tools_plugin.rs index e20b7d1ae..6ec8f0bee 100644 --- a/crates/mill-plugin-system/src/system_tools_plugin.rs +++ b/crates/mill-plugin-system/src/system_tools_plugin.rs @@ -12,9 +12,68 @@ use ignore::WalkBuilder; use mill_plugin_api::language::detect_package_manager; use serde::Deserialize; use serde_json::{json, Value}; +use std::net::{IpAddr, ToSocketAddrs}; use std::path::Path; use std::sync::Arc; use tracing::{debug, warn}; +use url::Url; + +/// Check if an IP address is allowed to be accessed by the web_fetch tool. +/// Blocks loopback, private, unspecified, multicast, broadcast, and link-local IPs. +fn is_allowed_ip(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(ipv4) => { + let octets = ipv4.octets(); + if octets[0] == 127 { + return false; + } + if octets[0] == 10 { + return false; + } + if octets[0] == 172 && (16..=31).contains(&octets[1]) { + return false; + } + if octets[0] == 192 && octets[1] == 168 { + return false; + } + if octets[0] == 0 { + return false; + } + if (224..=239).contains(&octets[0]) { + return false; + } + if octets == [255, 255, 255, 255] { + return false; + } + if octets[0] == 169 && octets[1] == 254 { + return false; + } + true + } + IpAddr::V6(ipv6) => { + if let Some(ipv4) = ipv6.to_ipv4_mapped() { + return is_allowed_ip(&IpAddr::V4(ipv4)); + } + if ipv6.is_loopback() { + return false; + } + if ipv6.is_unspecified() { + return false; + } + if ipv6.is_multicast() { + return false; + } + let segments = ipv6.segments(); + if (segments[0] & 0xfe00) == 0xfc00 { + return false; + } + if (segments[0] & 0xffc0) == 0xfe80 { + return false; + } + true + } + } +} /// System tools plugin for non-LSP workspace operations pub struct SystemToolsPlugin { @@ -307,14 +366,128 @@ fn handle_web_fetch(params: Value) -> PluginResult { debug!(url = %args.url, "Fetching URL content"); - // Use reqwest to fetch the URL content - let response = reqwest::blocking::get(&args.url).map_err(|e| PluginSystemError::IoError { - message: format!("Failed to fetch URL: {}", e), - })?; + let mut current_url_str = args.url.clone(); + let max_redirects = 5; + let mut redirect_count = 0; + let html_content: String; - let html_content = response.text().map_err(|e| PluginSystemError::IoError { - message: format!("Failed to read response text: {}", e), - })?; + loop { + if redirect_count >= max_redirects { + return Err(PluginSystemError::IoError { + message: "Too many redirects".to_string(), + }); + } + + let parsed_url = Url::parse(¤t_url_str).map_err(|e| PluginSystemError::IoError { + message: format!("Invalid URL: {}", e), + })?; + + // Only allow HTTP/HTTPS + let scheme = parsed_url.scheme(); + if scheme != "http" && scheme != "https" { + return Err(PluginSystemError::IoError { + message: "Unsupported URL scheme. Only HTTP and HTTPS are allowed.".to_string(), + }); + } + + let host = parsed_url + .host_str() + .ok_or_else(|| PluginSystemError::IoError { + message: "Missing host in URL".to_string(), + })?; + + let port = parsed_url + .port_or_known_default() + .unwrap_or(if scheme == "https" { 443 } else { 80 }); + + // Resolve IPs + // Note: For IPv6 literals we need to make sure the brackets are preserved before appending port + let host_for_resolution = if host.contains(':') && !host.starts_with('[') { + format!("[{}]:{}", host, port) + } else { + format!("{}:{}", host, port) + }; + + let addrs_iter = + host_for_resolution + .to_socket_addrs() + .map_err(|e| PluginSystemError::IoError { + message: format!("Failed to resolve hostname: {}", e), + })?; + + let addrs: Vec<_> = addrs_iter.collect(); + if addrs.is_empty() { + return Err(PluginSystemError::IoError { + message: "No IP addresses found for hostname".to_string(), + }); + } + + // Validate ALL resolved IPs to prevent DNS rebinding attacks mixing safe and malicious IPs + for addr in &addrs { + if !is_allowed_ip(&addr.ip()) { + return Err(PluginSystemError::IoError { + message: "Access to private/local IP address is blocked (SSRF Protection)" + .to_string(), + }); + } + } + + // Pin the connection to the first validated IP, disabling automatic redirects. + // This ensures the connection is made to a validated IP, mitigating DNS rebinding (TOCTOU) attacks, + // while preserving the original host for SNI and HTTP headers. + let pinned_ip = addrs[0]; + + let client = reqwest::blocking::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .resolve(host, pinned_ip) + .build() + .map_err(|e| PluginSystemError::IoError { + message: format!("Failed to build HTTP client: {}", e), + })?; + + let response = + client + .get(parsed_url.clone()) + .send() + .map_err(|e| PluginSystemError::IoError { + message: format!("Failed to fetch URL: {}", e), + })?; + + if response.status().is_redirection() { + if let Some(loc) = response.headers().get(reqwest::header::LOCATION) { + let loc_str = loc.to_str().map_err(|_| PluginSystemError::IoError { + message: "Invalid Location header".to_string(), + })?; + + let next_url = + parsed_url + .join(loc_str) + .map_err(|e| PluginSystemError::IoError { + message: format!("Invalid redirect URL: {}", e), + })?; + + current_url_str = next_url.into(); + redirect_count += 1; + continue; + } else { + return Err(PluginSystemError::IoError { + message: "Redirect missing Location header".to_string(), + }); + } + } + + if !response.status().is_success() { + return Err(PluginSystemError::IoError { + message: format!("HTTP error: {}", response.status()), + }); + } + + let content = response.text().map_err(|e| PluginSystemError::IoError { + message: format!("Failed to read response text: {}", e), + })?; + html_content = content; + break; + } // Convert HTML to Markdown for easier AI processing let markdown_content = html2md_rs::to_md::safe_from_html_to_md(html_content).map_err(|e| { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f89e1f05a..542f7244d 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -571,89 +571,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -742,36 +758,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -844,71 +866,85 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.2': resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -2126,48 +2162,56 @@ packages: engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: glibc sass-embedded-linux-arm@1.97.3: resolution: {integrity: sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] + libc: glibc sass-embedded-linux-musl-arm64@1.97.3: resolution: {integrity: sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: musl sass-embedded-linux-musl-arm@1.97.3: resolution: {integrity: sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] + libc: musl sass-embedded-linux-musl-riscv64@1.97.3: resolution: {integrity: sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] + libc: musl sass-embedded-linux-musl-x64@1.97.3: resolution: {integrity: sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: musl sass-embedded-linux-riscv64@1.97.3: resolution: {integrity: sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] + libc: glibc sass-embedded-linux-x64@1.97.3: resolution: {integrity: sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: glibc sass-embedded-unknown-all@1.97.3: resolution: {integrity: sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==}