Repository: denoland/deno
Pull request: feat: --allow-net supports CIDR ranges

Remove traffic to all local network with this PR using --deny-net=...

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->


Changed files:
filename: Cargo.lock
patch: @@ -2624,6 +2624,7 @@ dependencies = [
  "deno_terminal 0.2.2",
  "deno_unsync",
  "fqdn",
+ "ipnetwork",
  "libc",
  "log",
  "nix 0.27.1",

filename: cli/args/flags_net.rs
patch: @@ -137,7 +137,10 @@ mod tests {
       "localhost:8000",
       "0.0.0.0:4545",
       "127.0.0.1:4545",
-      "999.0.88.1:80"
+      "999.0.88.1:80",
+      "127.0.0.0/24",
+      "192.168.1.0/24",
+      "10.0.0.0/8"
     ];
     let expected = svec![
       "deno.land",
@@ -159,7 +162,10 @@ mod tests {
       "localhost:8000",
       "0.0.0.0:4545",
       "127.0.0.1:4545",
-      "999.0.88.1:80"
+      "999.0.88.1:80",
+      "127.0.0.0/24",
+      "192.168.1.0/24",
+      "10.0.0.0/8"
     ];
     let actual = parse(entries).unwrap();
     assert_eq!(actual, expected);

filename: runtime/permissions/Cargo.toml
patch: @@ -20,6 +20,7 @@ deno_path_util.workspace = true
 deno_terminal.workspace = true
 deno_unsync.workspace = true
 fqdn.workspace = true
+ipnetwork.workspace = true
 libc.workspace = true
 log.workspace = true
 once_cell.workspace = true

filename: runtime/permissions/lib.rs
patch: @@ -19,6 +19,7 @@ use deno_path_util::url_to_file_path;
 use deno_terminal::colors;
 use deno_unsync::sync::AtomicFlag;
 use fqdn::FQDN;
+use ipnetwork::IpNetwork;
 use once_cell::sync::Lazy;
 use parking_lot::Mutex;
 use serde::de;
@@ -831,6 +832,7 @@ pub enum Host {
   FqdnWithSubdomainWildcard(FQDN),
   Ip(IpAddr),
   Vsock(u32),
+  IpSubnet(IpNetwork),
 }
 
 #[derive(Debug, thiserror::Error, deno_error::JsError)]
@@ -879,6 +881,8 @@ impl Host {
         ));
       }
       Ok(Host::Ip(ip))
+    } else if let Ok(ip_subnet) = s.parse::<IpNetwork>() {
+      Ok(Host::IpSubnet(ip_subnet))
     } else {
       let lower = if s.chars().all(|c| c.is_ascii_lowercase()) {
         Cow::Borrowed(s)
@@ -969,6 +973,7 @@ impl QueryDescriptor for NetDescriptor {
       ) => a == b,
       (Host::Ip(a), Host::Ip(b)) => a == b,
       (Host::Vsock(a), Host::Vsock(b)) => a == b,
+      (Host::IpSubnet(a), Host::Ip(b)) => a.contains(*b),
       _ => false,
     }
   }
@@ -1148,6 +1153,7 @@ impl fmt::Display for NetDescriptor {
     match &self.0 {
       Host::Fqdn(fqdn) => write!(f, "{fqdn}"),
       Host::FqdnWithSubdomainWildcard(fqdn) => write!(f, "*.{fqdn}"),
+      Host::IpSubnet(ip_subnet) => write!(f, "{ip_subnet}"),
       Host::Ip(IpAddr::V4(ip)) => write!(f, "{ip}"),
       Host::Ip(IpAddr::V6(ip)) => write!(f, "[{ip}]"),
       Host::Vsock(cid) => write!(f, "vsock:{cid}"),
@@ -4992,6 +4998,31 @@ mod tests {
     }
   }
 
+  #[test]
+  fn test_net_ip_subnet() {
+    set_prompter(Box::new(TestPrompter));
+    let parser = TestPermissionDescriptorParser;
+    let perms = Permissions::from_options(
+      &parser,
+      &PermissionsOptions {
+        allow_net: Some(svec!["10.0.0.0/24"]),
+        deny_net: Some(svec!["192.168.1.0/24", "172.16.0.0/12"]),
+        ..Default::default()
+      },
+    )
+    .unwrap();
+    let mut perms = PermissionsContainer::new(Arc::new(parser), perms);
+    let cases = [
+      ("10.0.0.1", true),
+      ("192.168.1.1", false),
+      ("172.16.0.1", false),
+    ];
+
+    for (host, is_ok) in cases {
+      assert_eq!(perms.check_net(&(host, None), "api").is_ok(), is_ok);
+    }
+  }
+
   #[test]
   fn test_deserialize_child_permissions_arg() {
     set_prompter(Box::new(TestPrompter));

filename: tests/integration/run_tests.rs
patch: @@ -1438,6 +1438,28 @@ mod permissions {
     assert!(!err.contains(util::PERMISSION_DENIED_PATTERN));
   }
 
+  #[test]
+  fn net_fetch_deny_cidr() {
+    let _http_guard = util::http_server();
+    let (_, err) = util::run_and_collect_output(
+      false,
+        "run --allow-net --deny-net=192.168.0.0/16 run/complex_permissions_test.ts netFetch http://192.168.1.128:4545/",
+        None,
+        None,
+        false,
+      );
+    assert!(err.contains(util::PERMISSION_DENIED_PATTERN));
+
+    let (_, err) = util::run_and_collect_output(
+      true,
+        "run --allow-net --deny-net=192.168.0.0/16 run/complex_permissions_test.ts netFetch http://127.0.0.1:4545/",
+        None,
+        None,
+        true,
+      );
+    assert!(!err.contains(util::PERMISSION_DENIED_PATTERN));
+  }
+
   #[test]
   fn net_fetch_localhost_subdomain() {
     let _http_guard = util::http_server();