From e8f83590eeac2ae156d0f5f9a409b32f40bff2f1 Mon Sep 17 00:00:00 2001 From: caryoscelus Date: Tue, 18 Jul 2023 14:38:17 +0000 Subject: [PATCH 1/3] disable plugins in data dir --- src/Plugin/PluginManager.py | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py index dbafa98f..5855c842 100644 --- a/src/Plugin/PluginManager.py +++ b/src/Plugin/PluginManager.py @@ -17,7 +17,6 @@ class PluginManager: def __init__(self): self.log = logging.getLogger("PluginManager") self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__)) - self.path_installed_plugins = config.data_dir + "/__plugins__" self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class) self.subclass_order = {} # Record the load order of the plugins, to keep it after reload self.pluggable = {} @@ -93,34 +92,6 @@ class PluginManager: plugin["loaded"] = plugin_name in self.plugin_names plugins.append(plugin) - plugins += self.listInstalledPlugins(list_disabled) - return plugins - - def listInstalledPlugins(self, list_disabled=False): - plugins = [] - - for address, site_plugins in sorted(self.config.items()): - if address == "builtin": - continue - for plugin_inner_path, plugin_config in sorted(site_plugins.items()): - is_enabled = plugin_config.get("enabled", False) - if not is_enabled and not list_disabled: - continue - plugin_name = os.path.basename(plugin_inner_path) - - dir_path = "%s/%s/%s" % (self.path_installed_plugins, address, plugin_inner_path) - - plugin = {} - plugin["source"] = address - plugin["name"] = plugin_name - plugin["dir_name"] = plugin_name - plugin["dir_path"] = dir_path - plugin["inner_path"] = plugin_inner_path - plugin["enabled"] = is_enabled - plugin["rev"] = plugin_config.get("rev", 0) - plugin["loaded"] = plugin_name in self.plugin_names - plugins.append(plugin) - return plugins # Load all plugin @@ -156,10 +127,10 @@ class PluginManager: for module_name, module in list(sys.modules.items()): if not module or not getattr(module, "__file__", None): continue - if self.path_plugins not in module.__file__ and self.path_installed_plugins not in module.__file__: + if self.path_plugins not in module.__file__: continue - if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled + if not getattr(module, 'allow_reload', True): # Reload disabled # Re-add non-reloadable plugins for class_name, classes in self.plugins_before.items(): for c in classes: From 07b7c5824fa57fff95d35320878cb62b4c6bc234 Mon Sep 17 00:00:00 2001 From: caryoscelus Date: Fri, 21 Jul 2023 11:33:46 +0000 Subject: [PATCH 2/3] remove third-party plugin management refs #216 --- .../UiPluginManager/UiPluginManagerPlugin.py | 93 ------------------- 1 file changed, 93 deletions(-) diff --git a/plugins/UiPluginManager/UiPluginManagerPlugin.py b/plugins/UiPluginManager/UiPluginManagerPlugin.py index 1ab80f53..0bfa8ec5 100644 --- a/plugins/UiPluginManager/UiPluginManagerPlugin.py +++ b/plugins/UiPluginManager/UiPluginManagerPlugin.py @@ -126,96 +126,3 @@ class UiWebsocketPlugin(object): plugin_manager.saveConfig() return "ok" - - def pluginAction(self, action, address, inner_path): - site = self.server.sites.get(address) - plugin_manager = PluginManager.plugin_manager - - # Install/update path should exists - if action in ("add", "update", "add_request"): - if not site: - raise Exception("Site not found") - - if not site.storage.isDir(inner_path): - raise Exception("Directory not found on the site") - - try: - plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json") - plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"]) - except Exception as err: - raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err)) - - source_path = site.storage.getPath(inner_path) - - target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path - plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {}) - - # Make sure plugin (not)installed - if action in ("add", "add_request") and os.path.isdir(target_path): - raise Exception("Plugin already installed") - - if action in ("update", "remove") and not os.path.isdir(target_path): - raise Exception("Plugin not installed") - - # Do actions - if action == "add": - shutil.copytree(source_path, target_path) - - plugin_config["date_added"] = int(time.time()) - plugin_config["rev"] = plugin_info["rev"] - plugin_config["enabled"] = True - - if action == "update": - shutil.rmtree(target_path) - - shutil.copytree(source_path, target_path) - - plugin_config["rev"] = plugin_info["rev"] - plugin_config["date_updated"] = time.time() - - if action == "remove": - del plugin_manager.config[address][inner_path] - shutil.rmtree(target_path) - - def doPluginAdd(self, to, inner_path, res): - if not res: - return None - - self.pluginAction("add", self.site.address, inner_path) - PluginManager.plugin_manager.saveConfig() - - self.cmd( - "confirm", - ["Plugin installed!
You have to restart the client to load the plugin", "Restart"], - lambda res: self.actionServerShutdown(to, restart=True) - ) - - self.response(to, "ok") - - @flag.no_multiuser - def actionPluginAddRequest(self, to, inner_path): - self.pluginAction("add_request", self.site.address, inner_path) - plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json") - warning = "Warning!
Plugins has the same permissions as the ZeroNet client.
" - warning += "Do not install it if you don't trust the developer.
" - - self.cmd( - "confirm", - ["Install new plugin: %s?
%s" % (plugin_info["name"], warning), "Trust & Install"], - lambda res: self.doPluginAdd(to, inner_path, res) - ) - - @flag.admin - @flag.no_multiuser - def actionPluginRemove(self, to, address, inner_path): - self.pluginAction("remove", address, inner_path) - PluginManager.plugin_manager.saveConfig() - return "ok" - - @flag.admin - @flag.no_multiuser - def actionPluginUpdate(self, to, address, inner_path): - self.pluginAction("update", address, inner_path) - PluginManager.plugin_manager.saveConfig() - PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True - return "ok" From 14e8130acb4a092b143d0a8046f29926f28e1b25 Mon Sep 17 00:00:00 2001 From: caryoscelus Date: Tue, 14 Nov 2023 23:11:51 +0000 Subject: [PATCH 3/3] Rewrite cross-site and cross-host requests detection Make sure browsers send referrers so we can track cross-site requests (could be used to identify which sites user hosts) This breaks /raw because there are no referrers there fixes #227 fixes #223 fixes #224 --- src/Ui/UiRequest.py | 97 ++++++++++++++++++++++++++++-------- src/Ui/template/wrapper.html | 2 +- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 0f8437d6..fa03bdca 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -49,7 +49,7 @@ class SecurityError(Exception): @PluginManager.acceptPlugins -class UiRequest(object): +class UiRequest: def __init__(self, server, get, env, start_response): if server: @@ -99,8 +99,52 @@ class UiRequest(object): def resolveDomain(self, domain): return self.server.site_manager.resolveDomainCached(domain) - # Call the request handler function base on path + def isCrossOriginRequest(self): + """Prevent detecting sites on this 0net instance + + In particular, we block non-user requests from other hosts as well as + cross-site + """ + + url = self.getRequestUrl() + fetch_mode = self.env.get('HTTP_SEC_FETCH_MODE') + origin = self.env.get('HTTP_ORIGIN') + referer = self.env.get('HTTP_REFERER') + + # Allow all user-initiated requests + if fetch_mode == 'navigate': + return False + + # Deny requests that cannot be traced + if not origin and not referer: + return True + + # Deny requests from non-0net origins + if origin and not self.isSameHost(origin, url): + return True + + # Allow non-site specific requests + if self.getRequestSite() == '/': + return False + + # Deny cross site requests + if not self.isSameOrigin(referer, url): + return True + + return False + def route(self, path): + """Main routing + + If no internal action is performed, calls action[Path] from plugins + + This behaviour is not very flexible or easy to follow, so perhaps + we'd want something else.. + """ + + if self.isCrossOriginRequest(): + return self.error404() + # Restict Ui access by ip if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict: return self.error403(details=False) @@ -284,37 +328,43 @@ class UiRequest(object): is_script_nonce_supported = True return is_script_nonce_supported + def getRequestSite(self): + """Return 0net site addr associated with current request + + If request is site-agnostic, returns / + """ + path = self.env["PATH_INFO"] + match = re.match(r'(/raw)?(?P/1[a-zA-Z0-9]*)', path) + if not match: + match = re.match(r'(/raw)?/(?P[a-zA-Z0-9\.\-_]*)', path) + if match: + domain = match.group('domain') + if self.isDomain(domain): + addr = self.resolveDomain(domain) + return '/'+addr + return '/' + return match.group('site') + # Send response headers def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]): - url = self.getRequestUrl() - referer = self.env.get('HTTP_REFERER') - origin = self.env.get('HTTP_ORIGIN') - fetch_site = self.env.get('HTTP_SEC_FETCH_SITE') - fetch_mode = self.env.get('HTTP_SEC_FETCH_MODE') - not_same_ref = referer and not self.isSameHost(referer, url) - not_same_origin = origin and not self.isSameHost(origin, url) - cross_site_not_navigate = not referer and fetch_site == 'cross-site' and not fetch_mode == 'navigate' - if status != 404 and (not_same_ref or not_same_origin or cross_site_not_navigate): - # pretend nothing is here for third-party access - return self.error404() - headers = {} headers["Version"] = "HTTP/1.1" headers["Connection"] = "Keep-Alive" headers["Keep-Alive"] = "max=25, timeout=30" headers["X-Frame-Options"] = "SAMEORIGIN" + headers["Referrer-Policy"] = "same-origin" if noscript: headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';" elif script_nonce and self.isScriptNonceSupported(): - headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:".format(script_nonce) + headers["Content-Security-Policy"] = f"default-src 'none'; script-src 'nonce-{script_nonce}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:" if allow_ajax: headers["Access-Control-Allow-Origin"] = "null" if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access - headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range" + headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range, Referer" headers["Access-Control-Allow-Credentials"] = "true" # Download instead of display file types that can be dangerous @@ -624,15 +674,18 @@ class UiRequest(object): if not url_a or not url_b: return False - url_a = url_a.replace("/raw/", "/") - url_b = url_b.replace("/raw/", "/") + host_pattern = r'(?Phttp[s]?://.*?)(/|$)' - origin_pattern = "http[s]{0,1}://(.*?/).*" + match_a = re.match(host_pattern, url_a) + match_b = re.match(host_pattern, url_b) - origin_a = re.sub(origin_pattern, "\\1", url_a) - origin_b = re.sub(origin_pattern, "\\1", url_b) + if not match_a or not match_b: + return False - return origin_a == origin_b + host_a = match_a.group('host') + host_b = match_b.group('host') + + return host_a == host_b def isSameOrigin(self, url_a, url_b): """Check if 0net origin is the same""" diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index f65c5066..2cce69cf 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -74,7 +74,7 @@ else if (window.opener && window.opener.location.toString()) { - +