Commit Diff


commit - d04a1dedc45b2aabb2ec9bae57604939a196b9be
commit + b38936bbf89f342bc53320ba9f16f0854cdfbc63
blob - a636ec040dd42996f384f3d9894e6b126d0f2f53
blob + 146194c4142774a611adc3ce8fbff389c45ad55b
--- wp.rs
+++ wp.rs
@@ -21,6 +21,43 @@ use std::{
     path::{Path, PathBuf},
 };
 
+#[cfg(target_os = "openbsd")]
+mod sandbox {
+    use std::ffi::CString;
+    use std::os::raw::c_char;
+    use std::path::Path;
+
+    extern "C" {
+        fn pledge(promises: *const c_char, execpromises: *const c_char) -> i32;
+        fn unveil(path: *const c_char, permissions: *const c_char) -> i32;
+    }
+
+    pub fn do_unveil(path: &Path, perms: &str) {
+        let p = CString::new(path.to_str().expect("unveil: invalid path"))
+            .expect("unveil: nul in path");
+        let f = CString::new(perms).expect("unveil: nul in perms");
+        if unsafe { unveil(p.as_ptr(), f.as_ptr()) } == -1 {
+            let e = std::io::Error::last_os_error();
+            super::err(&format!("unveil {}: {}", path.display(), e));
+        }
+    }
+
+    pub fn lock_unveil() {
+        if unsafe { unveil(std::ptr::null(), std::ptr::null()) } == -1 {
+            let e = std::io::Error::last_os_error();
+            super::err(&format!("unveil lock: {}", e));
+        }
+    }
+
+    pub fn do_pledge(promises: &str) {
+        let p = CString::new(promises).expect("pledge: nul in promises");
+        if unsafe { pledge(p.as_ptr(), std::ptr::null()) } == -1 {
+            let e = std::io::Error::last_os_error();
+            super::err(&format!("pledge: {}", e));
+        }
+    }
+}
+
 const VERSION: &str = "0.1.0";
 const NAME: &str = "wp";
 const FAVICON: &[u8] = include_bytes!("assets/favicon.ico");
@@ -223,6 +260,9 @@ fn resolve_modules(
     let modules = vars.get("MODULES").cloned().unwrap_or_default();
 
     for module in modules.split_whitespace() {
+        if module.contains("..") {
+            continue;
+        }
         load_module(module, portsdir, cache);
         if let Some(modvars) = cache.get(module) {
             for (k, v) in modvars {
@@ -301,6 +341,14 @@ fn parse_makefile(path: &Path) -> Option<HashMap<Strin
     Some(vars)
 }
 
+fn safe_name(s: &str) -> bool {
+    !s.is_empty()
+        && !s.contains('/')
+        && !s.contains('\0')
+        && s != "."
+        && s != ".."
+}
+
 fn port_from_makefile(
     category: &str,
     portname: &str,
@@ -308,6 +356,9 @@ fn port_from_makefile(
     portsdir: &Path,
     modcache: &mut ModCache,
 ) -> Option<Port> {
+    if !safe_name(category) || !safe_name(portname) {
+        return None;
+    }
     let mf = portdir.join("Makefile");
     let mut vars = parse_makefile(&mf)?;
 
@@ -378,6 +429,9 @@ fn port_from_vars(
     portsdir: Option<&Path>,
 ) -> Option<Port> {
     let (category, name) = path.split_once('/')?;
+    if !safe_name(category) || !safe_name(name) {
+        return None;
+    }
     let comment = vars.get("COMMENT").filter(|s| !s.is_empty())?;
 
     let descr = if let Some(pd) = portsdir {
@@ -418,16 +472,17 @@ fn load_dump(path: &Path, portsdir: Option<&Path>) -> 
 
     for line in text.lines() {
         // format: category/port.VARIABLE=value
-        let Some(dot) = line.find('.') else {
+        // find the first '=' then look backwards for the '.'
+        let Some(eq_pos) = line.find('=') else {
             continue;
         };
+        let prefix = &line[..eq_pos];
+        let Some(dot) = prefix.rfind('.') else {
+            continue;
+        };
         let portpath = &line[..dot];
-        let rest = &line[dot + 1..];
-        let Some(eq) = rest.find('=') else {
-            continue;
-        };
-        let var = &rest[..eq];
-        let val = &rest[eq + 1..];
+        let var = &line[dot + 1..eq_pos];
+        let val = &line[eq_pos + 1..];
         by_port
             .entry(portpath.to_string())
             .or_default()
@@ -537,7 +592,7 @@ fn port_meta(p: &Port, depth: usize) -> String {
 }
 
 fn html_escape(s: &str) -> String {
-    if !s.contains(&['&', '<', '>', '"'][..]) {
+    if !s.contains(&['&', '<', '>', '"', '\''][..]) {
         return s.to_string();
     }
     let mut out = String::with_capacity(s.len() + 8);
@@ -547,6 +602,7 @@ fn html_escape(s: &str) -> String {
             '<' => out.push_str("&lt;"),
             '>' => out.push_str("&gt;"),
             '"' => out.push_str("&quot;"),
+            '\'' => out.push_str("&#x27;"),
             _ => out.push(c),
         }
     }
@@ -741,7 +797,10 @@ fn write_port_pages(ports: &[Port], outdir: &Path, ft:
             ));
         }
 
-        if !p.homepage.is_empty() {
+        if !p.homepage.is_empty()
+            && (p.homepage.starts_with("https://")
+                || p.homepage.starts_with("http://"))
+        {
             html.push_str(&format!(
                 "<p>Homepage: <a href=\"{}\">{}</a></p>\n",
                 html_escape(&p.homepage),
@@ -887,6 +946,10 @@ fn write_alpha(ports: &[Port], outdir: &Path, ft: &str
     }
 }
 
+fn sanitize_field(s: &str) -> String {
+    s.replace(['\t', '\n'], " ").replace('\r', "")
+}
+
 fn write_index_txt(ports: &[Port], outdir: &Path) {
     let mut out = String::new();
     for p in ports {
@@ -895,11 +958,11 @@ fn write_index_txt(ports: &[Port], outdir: &Path) {
         out.push('/');
         out.push_str(&p.name);
         out.push('\t');
-        out.push_str(&p.pkgname);
+        out.push_str(&sanitize_field(&p.pkgname));
         out.push('\t');
-        out.push_str(&p.comment);
+        out.push_str(&sanitize_field(&p.comment));
         out.push('\t');
-        out.push_str(&p.maintainer);
+        out.push_str(&sanitize_field(&p.maintainer));
         out.push('\n');
     }
     write_file(&outdir.join("INDEX.txt"), &out);
@@ -915,7 +978,9 @@ fn url_decode(s: &str) -> String {
             let hex = [hi, lo];
             if let Ok(s) = std::str::from_utf8(&hex) {
                 if let Ok(n) = u8::from_str_radix(s, 16) {
-                    out.push(n);
+                    if n != 0 {
+                        out.push(n);
+                    }
                     continue;
                 }
             }
@@ -930,12 +995,14 @@ fn url_decode(s: &str) -> String {
 }
 
 fn cgi_search(index_path: &Path) {
-    let qs = std::env::var("QUERY_STRING").unwrap_or_default();
-    let query = qs
+    let mut qs = std::env::var("QUERY_STRING").unwrap_or_default();
+    qs.truncate(1024);
+    let mut query = qs
         .split('&')
         .find_map(|p| p.strip_prefix("q=").map(url_decode))
         .unwrap_or_default()
         .to_lowercase();
+    query.truncate(128);
 
     print!("Content-Type: text/html\r\n\r\n");
 
@@ -987,6 +1054,9 @@ fn cgi_search(index_path: &Path) {
             None => continue,
         };
 
+        if count >= 200 {
+            break;
+        }
         html.push_str(&format!(
             "<tr><td><a href=\"/ports/{}/{}.html\">\
              {}</a></td><td>{}</td><td>{}</td></tr>\n",
@@ -1000,6 +1070,9 @@ fn cgi_search(index_path: &Path) {
     }
 
     html.push_str("</table>\n");
+    if count >= 200 {
+        html.push_str("<p>Results limited to 200.</p>\n");
+    }
     html.push_str(&format!(
         "<p>{} results for \"{}\"</p>\n",
         count,
@@ -1034,6 +1107,12 @@ fn main() {
     // CGI mode: QUERY_STRING is set by the HTTP server
     if std::env::var("QUERY_STRING").is_ok() {
         let index = PathBuf::from("INDEX.txt");
+        #[cfg(target_os = "openbsd")]
+        {
+            sandbox::do_unveil(&index, "r");
+            sandbox::lock_unveil();
+            sandbox::do_pledge("stdio rpath");
+        }
         cgi_search(&index);
         return;
     }
@@ -1072,6 +1151,21 @@ fn main() {
         i += 1;
     }
 
+    fs::create_dir_all(&outdir).unwrap_or_else(|e| {
+        errio(&outdir, &e);
+    });
+
+    #[cfg(target_os = "openbsd")]
+    {
+        sandbox::do_unveil(&portsdir, "r");
+        if let Some(ref d) = dumpfile {
+            sandbox::do_unveil(d, "r");
+        }
+        sandbox::do_unveil(&outdir, "rwc");
+        sandbox::lock_unveil();
+        sandbox::do_pledge("stdio rpath wpath cpath");
+    }
+
     let ports = if let Some(ref dump) = dumpfile {
         eprintln!("loading dump from {}", dump.display());
         let pd = if portsdir.is_dir() {