diff --git a/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md b/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md index 062b5f9b61..d161371265 100644 --- a/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md +++ b/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md @@ -122,6 +122,19 @@ localhost interface only. See Ghidra GUI Help Content related to BSim Database Configuration and `bsim_ctl` for more details. +### Ghidra Client - Server Allow List + +Ghidra client-side applications will now impose the use of a __Server Allow List__ mechanism to +help mitigate unintended server access. This mechanism is currently used to restrict: + +- Ghidra Server URL connections to unknown servers. Explicit repository access via a shared project + will cause that server to be implicitly added to the __Server Allow List__, and +- Clicking on URL links (e.g., http/https) within Ghidra listing comment annotations. + +See analyzeHeadlessREADME.md for information related to use of __analyzeHeadless__ and the new +__support/updateServerAllowList__ command which can be used to manage the __Server Allow List__ +entries. + ## BSim PostgreSQL Deployment and Control (bsim_ctl) Extensive changes have been made to the BSim PostgreSQL control script. New `bsim_ctl` commands diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html b/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html index 4b7946bd01..9a72dd46ef 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html +++ b/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html @@ -22,9 +22,9 @@ hyperlink.
-The following text shows the syntax of a sample URL annotation:
+The following text shows the syntax of a sample HTTP URL annotation:
- {@url "http://www.google.com" "Search Web"}
+ {@url "https://www.google.com" "Search Web"}
The bold text is required for all annotations. The italicized text is required but is @@ -38,7 +38,7 @@

- URL Annotation Example
The image above shows a URL annotation in its text form as entered into the EOL Comment
tab of the Comments dialog.
@@ -46,11 +46,13 @@
The image below shows how the annotation is rendered in Ghidra.

- Rendered URL Annotation Example
When the URL text (e.g., "http://www.google.com") in the above image is clicked from within +
When the URL text (e.g., "https://www.google.com") in the above image is clicked from within Ghidra, a web browser is launched and attempts to load the corresponding web page.
+ GHIDRA URL Annotation Example +If the URL text corresponds to a Ghidra URL and attempt will be made to open the referenced Program file within the Code Browser. Such a URL may refer to a Program file from a local project or Ghidra Server. The Ghidra URL forms supported include:
@@ -64,6 +66,14 @@ ghidra:/[<project-path>/]<project-name>?/<program-path>[#<address-or-symbol-ref>]
Clicking on all remote URL annotations will be
+ will be subject to the Server Allow List resulting in a possible confirmation dialog.
+ This is independent of possible SSL/TLS server authentication which may be required.
+ At anytime the Server Allow List may be cleared via the Project window
+ Edit->Clear Server Allow List... or managed using the support/updateServerAllowList shell script.
+ Execute this script without arguments to see usage information.
All annotations support double
+ quotes (") around content inside of the annotation tag, excluding the @name
+ part of the tag. Further, some annotations require quotes, as listed in the table above
+ (e.g., the Execute annotation requires quotes). It is considered good practice to
quote all annotation parameter values.
@@ -125,18 +123,18 @@ When you connect to the server the next time you run Ghidra, you will be prompted for the key-store password associated with this certificate key file. The path to your PKI certificate file is saved as part of your Ghidra preferences. --
- --
If the Ghidra Server, or other server, is not using PKI Certificates for user authentication, you can ignore this menu option since the certificate keystore will not be used.
-
Specifying the single user certificate + +
+ +
Setting your user PKI certificate key store + may also be required if Ghidra communicates with other web services which rely on PKI user + authentication.
+ (applies to Windows and macOS only).
Specifying the single user certificate keystore in this fashion will prevent the OS managed keystore from being used - (applied to Windows and macOS only).
++ +When attempting to communicate with various servers based upon a URL, you may be + prompted to either allow or disallow the connection and all future connection attempts. + The choice will be saved to a Server Allow List. A server entry is identified + by the associated communication protocol (e.g., "https", "ghidra"), its host name + as specified by a URL and the associated TCP port. The "ghidra" protocol is only + identified with its base-port (e.g., 13100) and not the other two related ports. + If you change your mind about a server connection it may be neccessary to revise this + saved Server Allow List. Two options exist for altering this list:
+ ++
+- To clear the entire Server Allow List from the Ghidra GUI Project Window, + choose Edit
+ +Clear Server Allow List....
- From a system command prompt, the updateServerAllowList command + can be used to view and selectively modify the allow-list. This command can + be found within the Ghidra installation support directory. + Server Allow List entries may be displayed with the -list + option, and added or modified using the -allow or -disallow options. + Entries may also removed entirely by using the -clear or -clearAll option.
+ +
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java index 8002dd10ad..39f08eb854 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java @@ -25,6 +25,7 @@ import ghidra.*; import ghidra.app.util.importer.LibrarySearchPathManager; import ghidra.app.util.opinion.Loader; import ghidra.framework.*; +import ghidra.framework.client.*; import ghidra.framework.model.DomainFolder; import ghidra.framework.protocol.ghidra.Handler; import ghidra.util.Msg; @@ -65,6 +66,7 @@ public class AnalyzeHeadless implements GhidraLaunchable { PASSWORD("-p", false), COMMIT("-commit", false, "[\"\"]]"), OK_TO_DELETE("-okToDelete", false), + ALLOW_ALL_ACCESS("-allowAllAccess", false), MAX_CPU("-max-cpu", true, " "), LIBRARY_SEARCH_PATHS("-librarySearchPaths", true, " [; ...]"), LOADER(Loader.COMMAND_LINE_ARG_PREFIX, true, " "), @@ -186,6 +188,16 @@ public class AnalyzeHeadless implements GhidraLaunchable { HeadlessOptions options = analyzer.getOptions(); parseOptions(options, args, optionStartIndex, ghidraURL, filesToImport); + // Ensure that we do not rely on prompting user for allowing server access + if (options.allowAllAccess) { + Msg.warn(AnalyzeHeadless.class, + "All remote server access is Allowed (" + Arg.ALLOW_ALL_ACCESS + ")"); + ClientUtil.setAllowListProvider(new AllowAllUrlAllowListProvider()); + } + else { + ClientUtil.setAllowListProvider(new DefaultlUrlAllowListProvider()); + } + Msg.info(AnalyzeHeadless.class, "Headless startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() + " ms)"); ClassSearcher.logStatistics(); @@ -199,6 +211,11 @@ public class AnalyzeHeadless implements GhidraLaunchable { analyzer.processLocal(args[0], projectName, rootFolderPath, filesToImport); } } + catch (IOException e) { + Msg.error(HeadlessAnalyzer.class, + "Abort due to error: " + e.getMessage()); + System.exit(EXIT_CODE_ERROR); + } catch (Throwable e) { Msg.error(HeadlessAnalyzer.class, "Abort due to Headless analyzer error: " + e.getMessage(), e); @@ -412,6 +429,9 @@ public class AnalyzeHeadless implements GhidraLaunchable { else if (checkArgument(Arg.OK_TO_DELETE, args, argi)) { options.setOkToDelete(true); } + else if (checkArgument(Arg.ALLOW_ALL_ACCESS, args, argi)) { + options.setAllowAllAccess(true); + } else if (checkArgument(Arg.LIBRARY_SEARCH_PATHS, args, argi)) { LibrarySearchPathManager.setLibraryPaths(args[++argi].split(";")); } @@ -584,4 +604,24 @@ public class AnalyzeHeadless implements GhidraLaunchable { private boolean isExistingArg(String s) { return Arrays.stream(Arg.values()).anyMatch(e -> e.matches(s)); } + + private static class AllowAllUrlAllowListProvider implements UrlAllowListProvider { + + @Override + public boolean isAllowed(URL url) { + return true; // do not cache decision + } + } + + private static class DefaultlUrlAllowListProvider extends AbstractUrlAllowListProvider { + + @Override + public boolean isAllowed(URL url) { + Boolean allowed = accessAllowed(url); + if (allowed != null) { + return allowed; + } + return false; // do not cache decision + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java index dd1079f761..853663d3a7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java @@ -36,8 +36,7 @@ import ghidra.app.util.importer.ProgramLoader; import ghidra.app.util.opinion.*; import ghidra.formats.gfilesystem.*; import ghidra.framework.*; -import ghidra.framework.client.ClientUtil; -import ghidra.framework.client.RepositoryAdapter; +import ghidra.framework.client.*; import ghidra.framework.data.*; import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; @@ -259,10 +258,8 @@ public class HeadlessAnalyzer { throws IOException, MalformedURLException, URISyntaxException { if (options.readOnly && options.commit) { - Msg.error(this, - "Abort due to Headless analyzer error: The requested readOnly option is in conflict " + + throw new IllegalArgumentException("The requested readOnly option is in conflict " + "with the commit option"); - return; } if (!"ghidra".equals(ghidraURL.getProtocol())) { @@ -270,9 +267,8 @@ public class HeadlessAnalyzer { } if (GhidraURL.isLocalURL(ghidraURL)) { - Msg.error(this, + throw new IllegalArgumentException( "Ghidra URL command form does not supported local project URLs (ghidra:/path...)"); - return; } String path = ghidraURL.getPath(); @@ -293,6 +289,22 @@ public class HeadlessAnalyzer { } } + if (!options.allowAllAccess) { + // Check Server Allow List - add access if not already blocked + Boolean hasServerAccess = UrlAllowListManager.getAccess(ghidraURL); + if (hasServerAccess == null) { + ServerSpecification serverSpec = ServerSpecification.get(ghidraURL); + Msg.info(HeadlessAnalyzer.class, + "NOTICE: Adding server to allow list: " + serverSpec.toString()); + UrlAllowListManager.updateAccess(ghidraURL, true); + } + else if (!hasServerAccess) { + ServerSpecification serverSpec = ServerSpecification.get(ghidraURL); + throw new IOException( + "Access denied by server allow list: " + serverSpec.toString()); + } + } + BundleHost bundleHost = GhidraScriptUtil.acquireBundleHostReference(); bundleHost.add(parseScriptPaths(options.scriptPaths), true, true); try { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessOptions.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessOptions.java index 01a83d6367..88e16f8441 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessOptions.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessOptions.java @@ -95,6 +95,10 @@ public class HeadlessOptions { // -p boolean allowPasswordPrompt; + // -allowAllAccess - Server Allow List will allow all remote server access, otherwise + // access may be restricted based upon previously allowed server access. + boolean allowAllAccess; + // -commit boolean commit; String commitComment; @@ -143,6 +147,7 @@ public class HeadlessOptions { keystore = null; connectUserID = null; allowPasswordPrompt = false; + allowAllAccess = false; commit = false; commitComment = null; okToDelete = false; @@ -392,6 +397,17 @@ public class HeadlessOptions { this.analyze = enabled; } + /** + * Remote Ghidra Server access relies on Server Allow List established by GUI application. + * This method can be used to allow all access and ignore Server Allow List. + * + * @param allowAccess True if all server access should be allowed, otherwise rely on + * Allow List previously cached by GUI application. + */ + public void setAllowAllAccess(boolean allowAccess) { + this.allowAllAccess = allowAccess; + } + /** * Sets the language and compiler spec from the provided input. Any null value will attempt * a "best-guess" if possible. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/UpdateServerAllowList.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/UpdateServerAllowList.java new file mode 100644 index 0000000000..eb2ba1b787 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/UpdateServerAllowList.java @@ -0,0 +1,143 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.headless; + +import java.io.IOException; +import java.net.*; +import java.util.*; + +import ghidra.GhidraApplicationLayout; +import ghidra.GhidraLaunchable; +import ghidra.framework.Application; +import ghidra.framework.ApplicationConfiguration; +import ghidra.framework.client.*; +import ghidra.framework.protocol.ghidra.Handler; +import ghidra.util.Msg; + +/** + * {@link UpdateServerAllowList} utility for managing the Server Allow List. + * See {@link UrlAllowListManager}. + */ +public class UpdateServerAllowList implements GhidraLaunchable { + + private static final String INVOCATION_NAME_PROPERTY = "UpdateServerAllowList.Name"; + + public UpdateServerAllowList() { + // Required for GhidraLaunchable + } + + private URL parseURL(String urlString) throws MalformedURLException { + URI uri = URI.create(urlString); + return uri.toURL(); + } + + private void checkMoreArgs(int currentArgIndex, String[] args) { + if (currentArgIndex == args.length - 1) { + usage(args); + } + } + + @Override + public void launch(GhidraApplicationLayout layout, String[] args) throws IOException { + Application.initializeApplication(layout, new ApplicationConfiguration()); + if (args.length == 0) { + usage(args); + } + + // NOTE: May need other protocol handlers to be registered to avoid URL exceptions + Handler.registerHandler(); + + try { + boolean printList = false; + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + switch (arg) { + + case "-allow": + checkMoreArgs(i, args); + URL url = parseURL(args[++i]); + UrlAllowListManager.updateAccess(url, true); + break; + + case "-disallow": + checkMoreArgs(i, args); + url = parseURL(args[++i]); + UrlAllowListManager.updateAccess(url, false); + break; + + case "-clear": + checkMoreArgs(i, args); + url = parseURL(args[++i]); + UrlAllowListManager.clearAccessEntry(url); + break; + + case "-clearAll": + UrlAllowListManager.clearAll(); + break; + + case "-list": + printList = true; + break; + + default: + usage(args); + } + } + + if (printList) { + Map accessMap = + UrlAllowListManager.getAccessMap(); + List servers = new ArrayList<>(accessMap.keySet()); + if (servers.isEmpty()) { + System.out.println("Server Allow List is empty."); + } + else { + Collections.sort(servers); + System.out.println("Server Allow List:"); + for (ServerSpecification svr : servers) { + + AccessRecord accessRecord = accessMap.get(svr); + String access = accessRecord.accessAllowed() ? "ALLOW " : "DISALLOW"; + Date date = new Date(accessRecord.time()); + + System.out.println( + " " + access + " " + svr.toUrlString() + " (" + date + ")"); + } + } + } + } + catch (Exception e) { + Msg.error("Exception processing Allow List updates", e); + } + System.out.println("Done"); + } + + private static void usage(String[] args) { + for (int i = 0; i < args.length; i++) { + System.err.println("arg " + i + ": " + args[i]); + } + String invocationName = System.getProperty(INVOCATION_NAME_PROPERTY); + + StringBuffer buf = new StringBuffer(); + buf.append("\nUsage: "); + buf.append( + invocationName != null ? invocationName : UpdateServerAllowList.class.getSimpleName()); + buf.append( + " [-allow :// : ] [-disallow :// : ] [-clear :// : ] [-clearAll] [-list]\n"); + System.err.println(buf.toString()); + System.exit(0); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java index 8f3545cbf1..4d0e12a27e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java @@ -18,7 +18,6 @@ package ghidra.app.util.task; import java.io.IOException; import java.net.URL; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; import docking.widgets.OptionDialog; import ghidra.app.plugin.core.progmgr.ProgramLocator; @@ -177,16 +176,20 @@ public class ProgramOpener { msg += "Please contact the Ghidra team for assistance."; Msg.showError(this, null, "Error Opening " + filename, msg); } - catch (Exception e) { - if (domainFile.isInWritableProject() && (e instanceof IOException)) { - RepositoryAdapter repo = domainFile.getParent().getProjectData().getRepository(); + catch (IOException e) { + RepositoryAdapter repo = domainFile.getParent().getProjectData().getRepository(); + if (repo != null && domainFile.isInWritableProject()) { ClientUtil.handleException(repo, e, "Open File", null); } else { Msg.showError(this, null, "Error Opening " + filename, - "Getting domain object failed.\n" + e.getMessage(), e); + "Getting domain object failed.\n" + e.getMessage()); } } + catch (Exception e) { + Msg.showError(this, null, "Error Opening " + filename, + "Getting domain object failed.\n" + e.getMessage(), e); + } return null; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java index 704e343b8f..552b493801 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java @@ -16,11 +16,14 @@ package ghidra.app.util.viewer.field; import java.net.*; +import java.util.Set; +import java.util.TreeSet; import docking.widgets.fieldpanel.field.AttributedString; import generic.theme.GThemeDefaults.Colors.Messages; import ghidra.app.nav.Navigatable; import ghidra.app.services.ProgramManager; +import ghidra.framework.client.ClientUtil; import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.program.model.listing.Program; @@ -33,9 +36,24 @@ import ghidra.util.Msg; * The first string will be treated as a Java {@link URL} and the optional second string will * be treated as display text. If there is not display text, then the URL will be * displayed. + * + * See {@link GhidraServerURLAnnotatedStringHandler} and {@link GhidraLocalURLAnnotatedStringHandler} + * for GHIDRA URL dummy handlers that are used to facilitate supported Comment Editor annotation types. */ public class URLAnnotatedStringHandler implements AnnotatedStringHandler { + private static final Set
allowedProtocols = new TreeSet<>(); + static { + // Set maintains alphabetical order or protocols + // The 'ghidra' protocol must be included here since only one shared + // annotation handler is used to process all supported URL protocols. + allowedProtocols.add("ghidra"); + allowedProtocols.add("http"); + allowedProtocols.add("https"); + } + + private static String allowedProtocolsStr = "ghidra, https or http"; + private static final String INVALID_SYMBOL_TEXT = "@url annotation must have a URL string optionally followed by a display string"; @@ -53,7 +71,15 @@ public class URLAnnotatedStringHandler implements AnnotatedStringHandler { URL url = getURLForString(text[1]); if (url == null) { - return new AttributedString("Invalid URL annotations - not a URL: " + text[1], + return new AttributedString("Invalid URL annotation - not a valid URL: " + text[1], + Messages.ERROR, prototypeString.getFontMetrics(0), false, Messages.ERROR); + } + + String protocol = url.getProtocol(); + if (!allowedProtocols.contains(protocol)) { + return new AttributedString( + "Unsupported URL annotation protocol - " + allowedProtocolsStr + " required:\n" + + text[1], Messages.ERROR, prototypeString.getFontMetrics(0), false, Messages.ERROR); } @@ -91,28 +117,45 @@ public class URLAnnotatedStringHandler implements AnnotatedStringHandler { String urlString = annotationParts[1]; URL url = getURLForString(urlString); if (url != null) { + + String protocol = url.getProtocol(); + if (!allowedProtocols.contains(protocol)) { + Msg.showError(this, null, "URL Access Not Allowed", + "Unsupported URL annotation protocol - " + allowedProtocolsStr + + " required:\n\n" + + urlString); + return false; + } + + if (!ClientUtil.getAllowListProvider().isAllowed(url)) { + Msg.showError(this, null, "URL Access Not Allowed", + "Access denied by Server Allow List"); + return false; + } + if (GhidraURL.PROTOCOL.equals(url.getProtocol())) { ProgramManager programManager = serviceProvider.getService(ProgramManager.class); return programManager.openProgram(url, ProgramManager.OPEN_CURRENT) != null; } + BrowserLoader.display(url, null, serviceProvider); return true; } Msg.showError(this, null, "Invalid URL", - "Unable to create a Java URL object from string: " + urlString); + "Invalid URL annotation - not a valid URL: " + urlString); return false; } @Override public String getDisplayString() { - return "URL"; + return "HTTP-URL"; } @Override public String getPrototypeString() { - return "{@url http://www.example.com}"; + return "{@url https://www.example.com}"; } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/BrowserLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/util/BrowserLoader.java index 2cc57a462c..6a20839a83 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/BrowserLoader.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/BrowserLoader.java @@ -21,8 +21,7 @@ import java.io.File; import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import docking.options.OptionsService; import ghidra.framework.options.OptionsChangeListener; @@ -36,15 +35,6 @@ import ghidra.framework.plugintool.ServiceProvider; */ public class BrowserLoader { - /** - * Display the content specified by url in a web browser window. This call will launch - * a new thread and then immediately return. - * @param url The URL to show. - */ - public static void display(URL url) { - display(url, null, null); - } - /** * Display the content specified by url in a web browser window. This call will launch * a new thread and then immediately return. @@ -57,6 +47,8 @@ public class BrowserLoader { if (url == null) { return; } + + Objects.requireNonNull(serviceProvider, "serviceProvider instance is required"); // open the browser in a new thread because the call may block (new Thread(new BrowserRunner(url, fileURL, serviceProvider))).start(); @@ -65,12 +57,7 @@ public class BrowserLoader { private static void displayFromBrowserRunner(URL url, URL fileURL, ServiceProvider serviceProvider) { try { - if (serviceProvider == null) { - displayBrowserForExternalURL(url); - } - else { - displayBrowser(url, fileURL, serviceProvider); - } + displayBrowser(url, fileURL, serviceProvider); } catch (Exception e) { Msg.showError(BrowserLoader.class, null, "Error Loading Browser", @@ -78,15 +65,6 @@ public class BrowserLoader { } } - private static void displayBrowserForExternalURL(URL url) throws Exception { - String[] arguments = - generateCommandArguments(url, null, - ManualViewerCommandWrappedOption.getDefaultBrowserLoaderOptions()); - Process p = Runtime.getRuntime().exec(arguments); - p.waitFor(); - p.exitValue(); // thought to help memory problems on some versions of windows - } - private static void displayBrowser(URL url, URL fileURL, ServiceProvider serviceProvider) { OptionsService service = serviceProvider.getService(OptionsService.class); ToolOptions options = diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/ManualViewerCommandWrappedOption.java b/Ghidra/Features/Base/src/main/java/ghidra/util/ManualViewerCommandWrappedOption.java index 7325362540..1207399799 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/ManualViewerCommandWrappedOption.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/ManualViewerCommandWrappedOption.java @@ -135,8 +135,8 @@ public class ManualViewerCommandWrappedOption implements CustomOption { option.setFileFormat(DEFAULT_URL_REPLACEMENT_STRING); } else if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.MAC_OS_X) { - option.setCommandString("open"); - option.setCommandArguments(new String[] {}); + option.setCommandString("anaconda-navigator"); + option.setCommandArguments(new String[] { "--url" }); option.setFileFormat(DEFAULT_URL_REPLACEMENT_STRING); } else { diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java index 600cc752b8..15bbcb74a2 100644 --- a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java @@ -21,11 +21,13 @@ import java.net.URL; import ghidra.app.CorePluginPackage; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.go.ipc.GhidraGoListener; +import ghidra.framework.client.ClientUtil; import ghidra.framework.main.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.util.Msg; +import ghidra.util.Swing; //@formatter:off @PluginInfo( @@ -50,49 +52,59 @@ public class GhidraGoPlugin extends Plugin implements ApplicationLevelOnlyPlugin } @Override - protected void init() { - super.init(); + protected void dispose() { + projectClosed(); + super.dispose(); } - @Override - protected void dispose() { + private void processUrl(URL url) { + + URL projectUrl = GhidraURL.getProjectURL(url); + Msg.info(this, "GhidraGo accepting the resource at " + projectUrl); + FrontEndTool frontEndTool = AppInfo.getFrontEndTool(); + + // Check for case where server access has already been blocked to + // launching tool and then failing to access program. + if (!ClientUtil.getAllowListProvider().isAllowed(url)) { + Msg.showError(this, frontEndTool.getActiveWindow(), "URL Access Not Allowed", + "Access denied by Server Allow List:\n" + projectUrl); + return; + } + + Swing.runLater(() -> { + frontEndTool.toFront(); + frontEndTool.accept(url); + }); + } + + private void projectOpened() { + projectClosed(); + try { + listener = new GhidraGoListener((url) -> processUrl(url)); + } + catch (IOException e) { + Msg.showError(this, null, "GhidraGoPlugin Exception", + "Unable to create GhidraGoListener", e); + } + } + + private void projectClosed() { if (this.listener != null) { listener.dispose(); listener = null; } - super.dispose(); } @Override public void processEvent(PluginEvent event) { if (event instanceof ProjectPluginEvent) { if (((ProjectPluginEvent) event).getProject() == null) { - dispose(); + projectClosed(); } else { - try { - listener = new GhidraGoListener((url) -> { - accept(url); - }); - } - catch (IOException e) { - Msg.showError(this, null, "GhidraGoPlugin Exception", - "Unable to create Listener", e); - } + projectOpened(); } } } - /** - * Accept the given url, which is then passed to the FrontEndTool to process. - * @param url a {@link GhidraURL} - * @return true if handled successfully, false otherwise. - */ - @Override - public boolean accept(URL url) { - Msg.info(this, "GhidraGo accepting the resource at " + GhidraURL.getProjectURL(url)); - FrontEndTool frontEndTool = AppInfo.getFrontEndTool(); - frontEndTool.toFront(); - return frontEndTool.accept(url); - } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/AbstractUrlAllowListProvider.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/AbstractUrlAllowListProvider.java new file mode 100644 index 0000000000..1fdcf0ee11 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/AbstractUrlAllowListProvider.java @@ -0,0 +1,89 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.client; + +import java.net.URISyntaxException; +import java.net.URL; + +import org.apache.commons.lang3.StringUtils; + +/** + * {@link AbstractUrlAllowListProvider} provides the abstract URL allow list provider. + * + * NOTE: See {@link UrlAllowListManager} for persistent allow list which will be consulted before + * prompting user and updated if user allows access. + */ +public abstract class AbstractUrlAllowListProvider implements UrlAllowListProvider { + + /** + * Determine if access has previously been determined and return access state. + * This method will return true for opaque or non-server URLs. + * + * @param url server URL + * @return true if access allowed or URL type is not handled by allow list, false + * if access is disallowed, or null if no allow list entry exists. + */ + protected static Boolean accessAllowed(URL url) { + try { + if (url.toURI().isOpaque() || StringUtils.isEmpty(url.getAuthority())) { + return true; + } + } + catch (URISyntaxException e) { + throw new IllegalArgumentException("Unsupported URL: " + url, e); + } + + return UrlAllowListManager.getAccess(url.getProtocol(), url.getHost(), getPort(url)); + } + + /** + * Get the port to be accessed by the specified URL. If not specified, the default port will + * be returned. + * + * @param url server URL + * @return URL port + */ + protected static int getPort(URL url) { + int port = url.getPort(); + if (port < 0) { + port = url.getDefaultPort(); + } + return port; + } + + + /** + * Get the base URL form which only includes protocol, server and port. + * + * @param url server URL + * @return simplified base URL (e.g., ghidra://host/repo ) + * @throws IllegalArgumentException if URL does not specify an authority + */ + protected static String getBaseURL(URL url) throws IllegalArgumentException { + + if (url.getAuthority() == null) { + throw new IllegalArgumentException("Invalid remote server URL: " + url); + } + + int port = url.getPort(); + if (port < 0) { + port = url.getDefaultPort(); + } + + return url.getProtocol() + "://" + url.getHost() + ":" + port; + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/AccessRecord.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/AccessRecord.java new file mode 100644 index 0000000000..0d4f6c6212 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/AccessRecord.java @@ -0,0 +1,26 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.client; + +/** + * {@link AccessRecord} provides the URL Allow List access record used by + * {@link UrlAllowListManager}. + * + * @param accessAllowed true if access is allowed, false if disallowed + * @param time date and time when change was made (milliseconds since January 1, 1970, 00:00:00 GMT). + * @see java.lang.System#currentTimeMillis() + */ +public record AccessRecord(boolean accessAllowed, long time) {} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java index 72adcb53ae..30b33370cc 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java @@ -18,6 +18,7 @@ package ghidra.framework.client; import java.awt.Component; import java.io.IOException; import java.net.Authenticator; +import java.net.URL; import java.rmi.*; import java.security.GeneralSecurityException; import java.util.Arrays; @@ -42,6 +43,7 @@ import ghidra.util.task.TaskMonitor; */ public class ClientUtil { + private static UrlAllowListProvider allowListProvider; private static ClientAuthenticator clientAuthenticator; private static Hashtable
serverHandles = new Hashtable<>(); @@ -49,6 +51,42 @@ public class ClientUtil { private ClientUtil() { } + /** + * Set client allow list provider for GhidraURL connections. + * @param provider allow list provider + */ + public static synchronized void setAllowListProvider( + UrlAllowListProvider provider) { + allowListProvider = provider; + } + + /** + * Get the currently installed GhidraURL allow list provider. If one has not been + * installed, this will trigger the installation of a default instance. The provider + * facilitates prompting the user if supported and returning a final allow or deny + * indication when accessing a URL. + * + * @return current allow list provider + */ + public static synchronized UrlAllowListProvider getAllowListProvider() { + if (allowListProvider == null) { + if (SystemUtilities.isInTestingMode()) { + // When testing disable the use of allow list. + // Specific tests can always set a specific provider if needed. + setAllowListProvider(new AbstractUrlAllowListProvider() { + @Override + public boolean isAllowed(URL url) { + return true; + } + }); + } + else { + setAllowListProvider(new DefaultGhidraUrlAllowListProvider()); + } + } + return allowListProvider; + } + /** * Set client authenticator * @param authenticator client authenticator instance @@ -65,7 +103,7 @@ public class ClientUtil { * installed, this will trigger the installation of a default instance. * @return current client authenticator */ - public static ClientAuthenticator getClientAuthenticator() { + public static synchronized ClientAuthenticator getClientAuthenticator() { if (clientAuthenticator == null) { if (SystemUtilities.isInHeadlessMode()) { setClientAuthenticator(new HeadlessClientAuthenticator()); @@ -83,6 +121,10 @@ public class ClientUtil { * prompted for a password via a Swing dialog. If a previous connection * attempt to this server failed, the adapter may be returned in a * disconnected state. + * + * NOTE: Requesting a Ghidra Server adapter using this method will automatically + * add the server to the Server Allow List. + * * @param host server name or address * @param port server port, 0 indicates that default port should be used. * @return repository server adapter @@ -95,6 +137,10 @@ public class ClientUtil { * Connect to a Repository Server and obtain a handle to it. * Based upon the server authentication requirements, the user may be * prompted for a password via a Swing dialog. + *
+ * NOTE: Requesting a Ghidra Server adapter using this method will automatically + * add the server to the Server Allow List. + * * @param host server name or address * @param port server port, 0 indicates that default port should be used. * @param forceConnect if true and the server adapter is disconnected, an @@ -225,7 +271,7 @@ public class ClientUtil { excMsg = exc.toString(); } if (exc instanceof IOException) { - Msg.showError(ClientUtil.class, parent, title, excMsg, exc); + Msg.showError(ClientUtil.class, parent, title, excMsg); } else { // show the stacktrace for non-IOException @@ -328,6 +374,13 @@ public class ClientUtil { static RemoteRepositoryServerHandle connect(ServerInfo server) throws LoginException, GeneralSecurityException, IOException, CancelledException { + // Check for explicitly denied server connections + Boolean access = + UrlAllowListManager.getAccess("ghidra", server.getServerName(), server.getPortNumber()); + if (access != null && !access) { + throw new NotConnectedException("Access denied by Server Allow List"); + } + getClientAuthenticator(); boolean allowLoginRetry = (clientAuthenticator instanceof DefaultClientAuthenticator); diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultGhidraUrlAllowListProvider.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultGhidraUrlAllowListProvider.java new file mode 100644 index 0000000000..cc0fd5315d --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultGhidraUrlAllowListProvider.java @@ -0,0 +1,104 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.client; + +import java.io.IOException; +import java.net.URL; + +import docking.widgets.OptionDialog; +import ghidra.util.SystemUtilities; + +/** + * {@link DefaultGhidraUrlAllowListProvider} is an allow list provider for cases where + * the user should be prompted when access has not yet been decided. + *
+ * Prompting uses {@code stdin/stdout} when {@link SystemUtilities#isInHeadlessMode()} returns true, + * otherwise Swing GUI is used via {@link OptionDialog} popup. + */ +public class DefaultGhidraUrlAllowListProvider extends AbstractUrlAllowListProvider { + + @Override + public boolean isAllowed(URL url) { + + Boolean allowed = accessAllowed(url); + if (allowed != null) { + return allowed; + } + + if (SystemUtilities.isInHeadlessMode()) { + String prompt = "Allow server access to " + getBaseURL(url) + " (y|n): "; + allowed = getConsoleYesNoUserResponse(prompt); + } + else { + int resp = OptionDialog.showOptionDialog(null, "Verify Server Access", + "Allow server access to " + getBaseURL(url) + + "?\nThis decision will be retained for subsequent access decisions.", + "&Yes", "&No", OptionDialog.QUESTION_MESSAGE); + if (resp == OptionDialog.CANCEL_OPTION) { + return false; // disallow connection without updating allow list + } + allowed = (resp == OptionDialog.OPTION_ONE); + } + + UrlAllowListManager.updateAccess(url, allowed); + return allowed; + } + + private boolean getConsoleYesNoUserResponse(String prompt) { + + try { + boolean closed = false; + while (!closed) { + + // Flush any stale stdin data before prompting for response + while (System.in.available() > 0) { + System.in.read(); + } + + // Prompt user on stdout + System.out.print(prompt); + + // Read response line from stdin + StringBuilder buf = new StringBuilder(); + while (true) { + int c = System.in.read(); + if (c <= 0) { + closed = true; + break; + } + if (c == '\r' || c == '\n') { + break; + } + buf.append((char) c); + } + + // Check for yes/no response + String resp = buf.toString(); + if ("y".equalsIgnoreCase(resp) || "yes".equalsIgnoreCase(resp)) { + return true; + } + if ("n".equalsIgnoreCase(resp) || "no".equalsIgnoreCase(resp)) { + return false; + } + System.out.println("Invalid response"); + } + } + catch (IOException e) { + // ignore + } + return false; + } +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java index 963dc5fdd4..1776d25962 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java @@ -60,6 +60,11 @@ public class RepositoryServerAdapter { /** * Construct a repository server interface adapter. + *
+ * NOTE: It is important that this method only be invoked for known/trusted Ghidra Servers. + * This instantiation will add the specified server to the cached Allow List to facilitate + * future access to the server via a Ghidra URL. + * * @param server provides server connection data */ RepositoryServerAdapter(ServerInfo server) { @@ -70,6 +75,7 @@ public class RepositoryServerAdapter { /** * Construct a repository server interface adapter. * @param serverHandle associated server handle (reconnect not supported) + * @param serverInfoString additional server information (e.g., URL) */ protected RepositoryServerAdapter(RepositoryServerHandle serverHandle, String serverInfoString) { @@ -146,6 +152,10 @@ public class RepositoryServerAdapter { } } + // Allow future server access when server is directly accessed + UrlAllowListManager.updateAccess("ghidra", server.getServerName(), server.getPortNumber(), + true); + lastConnectError = null; try { try { diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerSpecification.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerSpecification.java new file mode 100644 index 0000000000..d2c1e242cf --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerSpecification.java @@ -0,0 +1,70 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.client; + +import java.net.URL; +import java.util.Objects; + +/** + * {@link ServerSpecification} URL-based server specification record + * + * @param protocol URL protocol (e.g., {@code https}, {@code ghidra}). + * @param hostname host name or IP address + * @param port connection port (positive value) + */ +public record ServerSpecification(String protocol, String hostname, int port) + implements Comparable
{ + + public static ServerSpecification get(URL url) { + return new ServerSpecification(url.getProtocol(), url.getHost(), getPort(url)); + } + + static int getPort(URL url) { + int port = url.getPort(); + if (port < 0) { + port = url.getDefaultPort(); + } + return port; + } + + public ServerSpecification { + Objects.requireNonNull(protocol, "Protocol may not be null"); + Objects.requireNonNull(hostname, "Hostname may not be null"); + } + + @Override + public int compareTo(ServerSpecification o) { + int c = hostname.compareTo(o.hostname); + if (c != 0) { + return c; + } + c = Integer.compare(port, o.port); + if (c != 0) { + return c; + } + // NOTE: there should only be one protocol per server port + return protocol.compareTo(o.protocol); + } + + /** + * {@return server info in URL form} + */ + public String toUrlString() { + return protocol + "://" + hostname + ":" + port; + } + +} + diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/UrlAllowListManager.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/UrlAllowListManager.java new file mode 100644 index 0000000000..427c701bea --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/UrlAllowListManager.java @@ -0,0 +1,440 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.client; + +import java.io.*; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.util.*; + +import javax.swing.event.ChangeListener; + +import com.google.gson.*; + +import ghidra.framework.Application; +import ghidra.framework.store.local.LockFile; +import ghidra.util.Msg; +import ghidra.util.Swing; +import ghidra.util.datastruct.WeakDataStructureFactory; +import ghidra.util.datastruct.WeakSet; +import ghidra.util.exception.FileInUseException; +import utility.function.ExceptionalCallback; + +/** + * {@link UrlAllowListManager} provides management of the Server URL Allow List persistent cache. + */ +public class UrlAllowListManager { + + // Set false to disable allowing localhost access by default (intended for test use only) + public static boolean alwaysAllowLocalAccess = true; + + private static int FILE_FORMAT_VERSION = 1; + + private static final int LOCK_TIMEOUT_MS = 5000; // max time to wait for file lock + private static final long MIN_REFRESH_TIME_MS = 5000; // minimum time between file reads + + private static final String SERVER_ALLOW_LIST_FILENAME = "serverAllowList.json"; + private static final String SERVER_ALLOW_LIST_LOCKFILE = "serverAllowList.lock"; + + private static Map serverAccessMap; + private static LockFile lockFile; + private static long lastMod; // file modification when last read + private static long lastCheck; // system time when file last checked + + private static WeakSet changeListeners = + WeakDataStructureFactory.createCopyOnWriteWeakSet(); + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + /** + * Add listener to be notified when allow list changes are made. + * NOTE: This is intended for Swing use only and notification will be provided within + * the Swing thread. + * + * @param listener change listener + */ + public static synchronized void addChangeListener(ChangeListener listener) { + changeListeners.add(listener); + } + + /** + * Remove existing change listener. + * + * @param listener change listener + */ + public static synchronized void removeChangeListener(ChangeListener listener) { + changeListeners.remove(listener); + } + + private static boolean hasStaleAccessMap() { + if (serverAccessMap == null) { + return true; + } + long sysTime = System.currentTimeMillis(); + long timeSinceRead = System.currentTimeMillis() - lastCheck; + if (timeSinceRead < MIN_REFRESH_TIME_MS) { + // Avoid checking lastModified too often + return false; + } + lastCheck = sysTime; + return getAllowListFile().lastModified() != lastMod; + } + + private static Map getServerAccessMap() { + if (hasStaleAccessMap()) { + readServerAllowList(); + } + return serverAccessMap; + } + + /** + * Determine if the specified server access is allowed. + * This method will return true for all {@code localhost/127.0.0.1} URLs if + * {@code alwaysAllowLocalAccess} is true. + * + * @param protocol URL protocol + * @param host server host + * @param port server port + * @return true if server access is allowed, false if disallowed, null if no server entry was found + */ + public static synchronized Boolean getAccess(String protocol, String host, int port) { + + // Always allow access to localhost if alwaysAllowLocalAccess is true + if (alwaysAllowLocalAccess && ("localhost".equals(host) || "127.0.0.1".equals(host))) { + return true; + } + + try { + AccessRecord rec = + getServerAccessMap().get(new ServerSpecification(protocol, host, port)); + if (rec == null) { + return null; + } + return rec.accessAllowed(); + } + catch (IllegalArgumentException e) { + // NOTE: Caller must handle non-server URLs or opaque URLs + } + return false; + } + + /** + * Determine if the specified server access is allowed. + * This method will return true for opaque or non-server URLs. + * This method will return true for all {@code localhost/127.0.0.1} URLs if + * {@code alwaysAllowLocalAccess} is true. + * + * @param url server URL + * @return true if server access is allowed, false if disallowed, null if no server entry was found + */ + public static synchronized Boolean getAccess(URL url) { + + try { + if (url.toURI().isOpaque() || url.getAuthority() == null) { + return true; + } + } + catch (URISyntaxException e) { + throw new IllegalArgumentException("Unsupported URL: " + url, e); + } + + return getAccess(url.getProtocol(), url.getHost(), ServerSpecification.getPort(url)); + } + + /** + * Update the access for the specified server within the cached allow list. + * Changes to {@code localhost/127.0.0.1} server access will be ignored if + * {@code alwaysAllowLocalAccess} is true. + * + * @param protocol URL protocol (e.g., {@code https}, {@code ghidra}, {@code ghidra: }). + * @param host host name or IP address + * @param port connection port (positive value) + * @param allowAccess true allows access, false disallows access + * @throws IllegalArgumentException if an invalid parameter is specified + */ + public static synchronized void updateAccess(String protocol, String host, int port, + boolean allowAccess) + throws IllegalArgumentException { + + if (alwaysAllowLocalAccess && ("localhost".equals(host) || "127.0.0.1".equals(host))) { + return; + } + + ServerSpecification server = new ServerSpecification(protocol, host, port); + AccessRecord accessRec = + new AccessRecord(allowAccess, System.currentTimeMillis()); + + try { + withLock(() -> { + if (getAccess(protocol, host, port) != Boolean.valueOf(allowAccess)) { + Map map = getServerAccessMap(); + map.put(server, accessRec); + + writeServerAllowList(map); + + Msg.info(UrlAllowListManager.class, "Server Allow List has been updated: " + + (allowAccess ? "ALLOW " : "DISALLOW ") + server.toUrlString()); + } + }); + } + catch (IOException e) { + Msg.error(UrlAllowListManager.class, "Failed to update Server Allow List (" + + e.getMessage() + "): " + getAllowListFile()); + } + } + + /** + * Update the access for the specified server URL within the cached allow list. + * Changes to {@code localhost/127.0.0.1} server access will be ignored if + * {@code alwaysAllowLocalAccess} is true. + * + * @param url remote server URL (local and opaque URLs not permitted) + * @param allowAccess true allows access, false disallows access + * @throws IllegalArgumentException if an invalid, local or opaque URL is specified. + */ + public static synchronized void updateAccess(URL url, boolean allowAccess) + throws IllegalArgumentException { + + try { + if (url.toURI().isOpaque() || url.getAuthority() == null) { + throw new IllegalArgumentException("Server URL required"); + } + } + catch (URISyntaxException e) { + throw new IllegalArgumentException("Server URL required", e); + } + + updateAccess(url.getProtocol(), url.getHost(), ServerSpecification.getPort(url), + allowAccess); + } + + /** + * Clear the access entry for the specified server. This is intended to be used along with + * {@link #getAccessMap()} for managing the existing access map. + * Changes to {@code localhost/127.0.0.1} server access will be ignored if + * {@code alwaysAllowLocalAccess} is true. + * + * @param server server entry to be removed + */ + public static synchronized void clearAccessEntry(ServerSpecification server) { + + if ("localhost".equals(server.hostname()) || "127.0.0.1".equals(server.hostname())) { + return; + } + + try { + withLock(() -> { + Map map = getServerAccessMap(); + if (map.remove(server) != null) { + writeServerAllowList(map); + + Msg.info(UrlAllowListManager.class, + "Server Allow List has been updated: CLEAR " + + server.toUrlString()); + } + }); + } + catch (IOException e) { + Msg.error(UrlAllowListManager.class, "Failed to update Server Allow List (" + + e.getMessage() + "): " + getAllowListFile()); + } + } + + /** + * Clear the access entry for the specified server URL. + * Changes to {@code localhost/127.0.0.1} server access will be ignored if + * {@code alwaysAllowLocalAccess} is true. + * + * @param url remote server URL (local and opaque URLs not permitted) + */ + public static synchronized void clearAccessEntry(URL url) { + + try { + if (url.toURI().isOpaque() || url.getAuthority() == null) { + throw new IllegalArgumentException("Server URL required"); + } + } + catch (URISyntaxException e) { + throw new IllegalArgumentException("Server URL required", e); + } + + clearAccessEntry(ServerSpecification.get(url)); + } + + /** + * Clear all server access entries. + */ + public static synchronized void clearAll() { + try { + withLock(() -> { + serverAccessMap = new HashMap<>(); + writeServerAllowList(serverAccessMap); + Msg.info(UrlAllowListManager.class, "Server Allow List has been cleared."); + }); + } + catch (IOException e) { + Msg.error(UrlAllowListManager.class, "Failed to update Server Allow List (" + + e.getMessage() + "): " + getAllowListFile()); + } + } + + /** + * {@return copy of current server access map} + */ + public static synchronized Map getAccessMap() { + Map map = getServerAccessMap(); + return new HashMap<>(map); + } + + private static File getAllowListFile() { + return new File(Application.getUserSettingsDirectory(), SERVER_ALLOW_LIST_FILENAME); + } + + private static void withLock(ExceptionalCallback mapModifier) throws IOException { + LockFile fileLock = getFileLock(); + try { + lock(fileLock); + mapModifier.call(); + } + finally { + if (fileLock.haveLock()) { + fileLock.removeLock(); + } + } + } + + private static void writeServerAllowList(Map map) + throws IOException { + File file = getAllowListFile(); + try { + AllowListFile contentWrapper = AllowListFile.fromMap(map); + try (Writer w = new FileWriter(file)) { + w.write(GSON.toJson(contentWrapper)); + } + } + catch (IOException e) { + throw new IOException( + "Failed to store Server Allow List (" + e.getMessage() + "): " + file); + } + finally { + Swing.runLater(() -> { + for (ChangeListener listener : changeListeners) { + listener.stateChanged(null); + } + }); + } + } + + /** + * Read map from file. + */ + private static void readServerAllowList() { + File file = getAllowListFile(); + try { + withLock(() -> { + serverAccessMap = new HashMap<>(); + if (!file.exists() || file.length() == 0) { + return; + } + lastCheck = System.currentTimeMillis(); + lastMod = file.lastModified(); + String json = Files.readString(file.toPath()); + AllowListFile contentWrapper = GSON.fromJson(json, AllowListFile.class); + if (contentWrapper.version != FILE_FORMAT_VERSION) { + throw new IOException("Unsupported version: " + contentWrapper.version); + } + serverAccessMap.putAll(contentWrapper.toMap()); + }); + } + catch (IOException | JsonParseException e) { + e.printStackTrace(); + Msg.error(UrlAllowListManager.class, + "Failed to read Server Allow List - file may be replaced (" + e.getMessage() + + "): " + file); + } + finally { + Swing.runLater(() -> { + for (ChangeListener listener : changeListeners) { + listener.stateChanged(null); + } + }); + } + } + + private UrlAllowListManager() { + // no construct allowed + } + + /** + * Obtain a lock hold on the allow list file for reading or writing. + * @throws FileInUseException if lock is already active and failed to acquire + */ + private static void lock(LockFile fileLock) + throws FileInUseException { + if (!lockFile.createLock(LOCK_TIMEOUT_MS, true)) { + String msg = "File is in use - '" + lockFile + "'"; + String user = lockFile.getLockOwner(); + if (user != null) { + msg += " by " + user; + } + throw new FileInUseException(msg); + } + } + + /** + * {@return server allow list lock file} + */ + private static LockFile getFileLock() { + if (lockFile == null) { + lockFile = + new LockFile(Application.getUserSettingsDirectory(), SERVER_ALLOW_LIST_LOCKFILE); + } + return lockFile; + } + + /** + * {@link ServerAccessRecord} combines {@link ServerSpecification} and {@link AccessRecord} into + * a single record for json serialization use only. + */ + private static record ServerAccessRecord(String protocol, String hostname, int port, + boolean accessAllowed, long time) {} + + /** + * {@link AllowListFile} provides a json serialization wrapper with version to facilitate + * storage in list form. + */ + private static record AllowListFile(int version, List allowList) { + + static AllowListFile fromMap(Map map) { + List allowList = new ArrayList<>(); + map.forEach( + (svrSpec, access) -> allowList.add(new ServerAccessRecord(svrSpec.protocol(), + svrSpec.hostname(), svrSpec.port(), access.accessAllowed(), access.time()))); + return new AllowListFile(FILE_FORMAT_VERSION, allowList); + } + + Map toMap() { + Map map = new HashMap<>(); + if (allowList != null) { + allowList.forEach( + rec -> map.put(new ServerSpecification(rec.protocol, rec.hostname, rec.port), + new AccessRecord(rec.accessAllowed, rec.time))); + } + return map; + } + } +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/UrlAllowListProvider.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/UrlAllowListProvider.java new file mode 100644 index 0000000000..d4ec21db9e --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/UrlAllowListProvider.java @@ -0,0 +1,34 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.client; + +import java.net.URL; + +/** + * {@link UrlAllowListProvider} provides the URL allow list provider interface which facilitates + * obtaining an access decision for a specified server URL. + */ +public interface UrlAllowListProvider { + + /** + * Check if a server connection is permitted. If permitted, the server will be added + * to the cached allow list. This method will return true for opaque or non-server URLs. + * + * @param url server URL + * @return true if connection is allowed and may proceed, else false if denied + */ + public abstract boolean isAllowed(URL url); +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/model/ServerInfo.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/model/ServerInfo.java index 047144bd6e..c3bd9aae19 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/model/ServerInfo.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/model/ServerInfo.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,6 +17,8 @@ package ghidra.framework.model; import java.io.Serializable; +import org.apache.commons.lang3.StringUtils; + /** * Container for a host name and port number. * @@ -32,6 +34,12 @@ public class ServerInfo implements Serializable { * @param portNumber port number */ public ServerInfo(String host, int portNumber) { + if (portNumber <= 0) { + throw new IllegalArgumentException("Invalid port number specified: " + portNumber); + } + if (StringUtils.isBlank(host)) { + throw new IllegalArgumentException("Invalid host specified: '" + host + "'"); + } this.host = host; this.portNumber = portNumber; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java index ce4aee0b1f..d35a9d5660 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java @@ -26,6 +26,7 @@ import generic.timer.GhidraSwinglessTimer; import ghidra.framework.client.*; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.framework.remote.GhidraServerHandle; import ghidra.framework.remote.User; import ghidra.framework.store.*; import ghidra.framework.store.FileSystem; @@ -111,8 +112,7 @@ public class DefaultProjectData implements ProjectData { * @throws FileNotFoundException if project directory not found */ public DefaultProjectData(ProjectLocator localStorageLocator, boolean isInWritableProject, - boolean resetOwner) - throws NotFoundException, NotOwnerException, IOException, LockException { + boolean resetOwner) throws NotFoundException, NotOwnerException, IOException, LockException { localStorageLocator.checkProjectExistence(); this.localStorageLocator = localStorageLocator; boolean success = false; @@ -466,7 +466,13 @@ public class DefaultProjectData implements ProjectData { versionedFileSystemDir.getAbsolutePath(), create, true, !isInWritableProject, true); } else { - int port = properties.getInt(PORT_NUMBER, -1); + int port = properties.getInt(PORT_NUMBER, GhidraServerHandle.DEFAULT_PORT); + + if (isInWritableProject) { + // Ensure that future server access is allowed since it has been deliberately accessed + UrlAllowListManager.updateAccess("ghidra", serverName, port, true); + } + repository = getRepositoryAdapter(serverName, port, isInWritableProject); versionedFileSystem = new RemoteFileSystem(repository); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java index 5101c16598..7c11001f8d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/EditActionManager.java @@ -17,6 +17,8 @@ package ghidra.framework.main; import java.io.File; +import javax.swing.event.ChangeListener; + import docking.action.DockingAction; import docking.action.builder.ActionBuilder; import docking.tool.ToolConstants; @@ -24,6 +26,7 @@ import docking.widgets.OptionDialog; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; import ghidra.framework.OperatingSystem; +import ghidra.framework.client.UrlAllowListManager; import ghidra.framework.main.certs.CertificateManagerLauncher; import ghidra.net.DefaultKeyManagerFactory; import ghidra.net.PKIUtils; @@ -44,12 +47,25 @@ class EditActionManager { private FrontEndPlugin plugin; private FrontEndTool tool; + private ChangeListener serverAllowListListener = e -> serverAllowListChanged(); + + private DockingAction clearServerAllowList; private DockingAction clearCertPathAction; EditActionManager(FrontEndPlugin plugin) { this.plugin = plugin; tool = (FrontEndTool) plugin.getTool(); createActions(); + + UrlAllowListManager.addChangeListener(serverAllowListListener); + } + + void dispose() { + UrlAllowListManager.removeChangeListener(serverAllowListListener); + } + + private void serverAllowListChanged() { + clearServerAllowList.setEnabled(!UrlAllowListManager.getAccessMap().isEmpty()); } /** @@ -96,6 +112,22 @@ class EditActionManager { .build(); tool.addAction(clearCertPathAction); + clearServerAllowList = + new ActionBuilder("Clear Server Allow List", plugin.getName()).menuGroup("SvrAllowList") + .menuPath(ToolConstants.MENU_EDIT, "Clear Server Allow List...") + .helpLocation(new HelpLocation("FrontEndPlugin", "Clear_Server_Allow_List")) + .onAction(c -> clearServerAllowList()) + .enabledWhen(c -> DefaultKeyManagerFactory.getKeyStore() != null) + .enabled(true) + .build(); + tool.addAction(clearServerAllowList); + } + + private void clearServerAllowList() { + if (OptionDialog.YES_OPTION == OptionDialog.showYesNoDialog(tool.getToolFrame(), + "Clear Server Allow List", "Clear all Server Allow List entries?\n")) { + UrlAllowListManager.clearAll(); + } } /** diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java index 76ab81853f..11d2f9d466 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java @@ -129,6 +129,8 @@ public class FrontEndPlugin extends Plugin private FrontEndProvider frontEndProvider; + private EditActionManager editActionManager; + private ProjectRepoConnectAction repoConnectAction; private ProjectDataCutAction cutAction; private ClearCutAction clearCutAction; @@ -179,7 +181,7 @@ public class FrontEndPlugin extends Plugin tool.addComponentProvider(frontEndProvider, true); tool.setDefaultComponent(frontEndProvider); - new EditActionManager(this); + editActionManager = new EditActionManager(this); buildGui(); toolChestChangeListener = new MyToolChestChangeListener(); @@ -835,6 +837,7 @@ public class FrontEndPlugin extends Plugin dataTablePanel.dispose(); dataTreePanel.dispose(); projectActionManager.dispose(); + editActionManager.dispose(); } private void buildPanels() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java index 1d10bb472a..5d2f53aedd 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java @@ -241,8 +241,11 @@ class RepositoryChooser extends ReusableDialogComponentProvider { listModel.clear(); + String serverName = serverInfoComponent.getServerName(); + int port = serverInfoComponent.getPortNumber(); + RepositoryServerAdapter repositoryServer = ClientUtil.getRepositoryServer( - serverInfoComponent.getServerName(), serverInfoComponent.getPortNumber(), true); + serverName, port, true); if (repositoryServer == null) { return; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ServerInfoComponent.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ServerInfoComponent.java index 12e45aa548..bd1ee88183 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ServerInfoComponent.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ServerInfoComponent.java @@ -190,14 +190,14 @@ public class ServerInfoComponent extends JPanel { } private boolean checkPortNumber() { - portNumber = -1; + portNumber = GhidraServerHandle.DEFAULT_PORT; String portStr = portNumberField.getText(); String msg = null; try { portNumber = Integer.parseInt(portStr); - if (portNumber < 0 || portNumber > 65536) { - portNumber = -1; - msg = "Port number must in range of 0 to 65536"; + if (portNumber < 1 || portNumber > 65536) { + portNumber = GhidraServerHandle.DEFAULT_PORT; + msg = "Port number must in range of 1 to 65536"; } } catch (NumberFormatException e) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java index 4bd3e21f19..5d13d2aefc 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java @@ -257,13 +257,29 @@ public class DefaultProject implements Project { c.setReadOnly(true); StatusCode responseCode = c.getStatusCode(); - if (responseCode == StatusCode.NOT_FOUND) { - throw new IOException( - "Project/repository not found: " + GhidraURL.getDisplayString(url)); - } - if (responseCode == StatusCode.UNAUTHORIZED) { - // assume already informed - return null; + switch (responseCode) { + case OK: + break; + + case UNAUTHORIZED: + throw new IOException("Authorization failure"); + + case NOT_FOUND: + throw new IOException("Project or repository not found"); + + case LOCKED: + // Local projects are only accessed read-only, this condition should not occur + throw new AssertionError("Unexpected local project lock condition"); + + case FORBIDDEN: + throw new IOException("Access denied by Server Allow List"); + + case UNAVAILABLE: + throw new IOException("Server connection error occured (see log files)"); + + default: + throw new IOException("Server connection error occured: " + responseCode); + } DefaultProjectData veiwedProjectData = (DefaultProjectData) c.getProjectData(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java index db6fa89e6f..fbf8d289c0 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -61,8 +61,13 @@ public class DefaultGhidraProtocolConnector extends GhidraProtocolConnector { this.readOnly = readOnlyAccess; - statusCode = StatusCode.UNAVAILABLE; // if uncaught exception occurs + // Check for explicitly denied server connections + if (!ClientUtil.getAllowListProvider().isAllowed(url)) { + statusCode = StatusCode.FORBIDDEN; + return statusCode; + } + statusCode = StatusCode.UNAVAILABLE; repositoryServerAdapter = ClientUtil.getRepositoryServer(url.getHost(), url.getPort(), true); if (!repositoryServerAdapter.isConnected()) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java index e647d0beee..ea0661d42d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java @@ -35,6 +35,11 @@ public class GhidraURLConnection extends URLConnection { * This status code occurs when repository access is denied. */ UNAUTHORIZED(401, "Unauthorized"), + /** + * Ghidra Status-Code 403: Forbidden by client allow list. + * This status code occurs when repository access is denied. + */ + FORBIDDEN(403, "Forbidden"), /** * Ghidra Status-Code 404: Not Found. * This status code occurs when repository or project does not exist. @@ -49,6 +54,8 @@ public class GhidraURLConnection extends URLConnection { * Ghidra Status-Code 503: Unavailable. * This status code includes a variety of connection errors * which are reported/logged by the Ghidra Server support code. + * This error may also occur when the connection is not allowed + * by the user (see {@link GhidraURLAllowListProvider}). */ UNAVAILABLE(503, "Unavailable"); @@ -310,10 +317,16 @@ public class GhidraURLConnection extends URLConnection { // will be established with a RepositoryAdapter supplied by the connector. // Obtain connected transient project for repository and complete connection - TransientProjectManager transientProjectManager = - TransientProjectManager.getTransientProjectManager(); - TransientProjectData transientProjectData = - transientProjectManager.getTransientProject(protocolConnector, readOnly); + TransientProjectData transientProjectData = null; + try { + TransientProjectManager transientProjectManager = + TransientProjectManager.getTransientProjectManager(); + transientProjectData = + transientProjectManager.getTransientProject(protocolConnector, readOnly); + } + catch (IOException e) { + // ignore - rely on status code + } connected = true; statusCode = protocolConnector.getStatusCode(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java index 44d5538694..4495c9005a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java @@ -167,6 +167,9 @@ public class GhidraURLQuery { status = c.getStatusCode(); } catch (IOException e) { + if (status == null) { + status = StatusCode.UNAVAILABLE; + } resultHandler.handleError("URL Connection Error", e.getMessage(), ghidraUrl, e); } @@ -183,24 +186,35 @@ public class GhidraURLQuery { return; case NOT_FOUND: - generatedErr = new IOException("Project or repository not found"); + generatedErr = new IOException( + "Project or repository not found: " + getGhidraUrlDetail(ghidraUrl)); break; case LOCKED: // Local projects are only accessed read-only, this condition should not occur - throw new AssertionError("Unexpected local project lock condition"); + throw new AssertionError("Unexpected local project lock condition: " + + getGhidraUrlDetail(ghidraUrl)); + + case FORBIDDEN: + generatedErr = new IOException( + "Access denied by Server Allow List: " + getGhidraUrlDetail(ghidraUrl)); + break; case UNAVAILABLE: generatedErr = - new IOException("Server connection error occured (see log files)"); + new IOException("Server connection error occured (see log files): " + + getGhidraUrlDetail(ghidraUrl)); break; default: + generatedErr = new IOException("Server connection error occured (" + status + + ": " + getGhidraUrlDetail(ghidraUrl)); + break; } if (generatedErr != null) { - resultHandler.handleError("Content Not Found", generatedErr.getMessage(), ghidraUrl, - generatedErr); + resultHandler.handleError("Content Access Failure", generatedErr.getMessage(), + ghidraUrl, generatedErr); return; } @@ -234,6 +248,15 @@ public class GhidraURLQuery { } } + private String getGhidraUrlDetail(URL ghidraUrl) { + try { + return GhidraURL.getProjectURL(ghidraUrl).toString(); + } + catch (Exception e) { + return ghidraUrl.toString(); + } + } + private void processContent(Object content, TaskMonitor monitor) throws IOException, CancelledException { if (content instanceof DomainFile file) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java index b0f0d58af6..d303c7f69f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/Handler.java @@ -20,6 +20,7 @@ import java.net.*; import java.util.*; import ghidra.framework.client.ClientUtil; +import ghidra.framework.remote.GhidraServerHandle; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.exception.NotFoundException; @@ -111,6 +112,11 @@ public class Handler extends URLStreamHandler { return protocolHandler; } + @Override + protected int getDefaultPort() { + return GhidraServerHandle.DEFAULT_PORT; + } + @Override protected URLConnection openConnection(URL url) throws IOException { diff --git a/Ghidra/RuntimeScripts/certification.manifest b/Ghidra/RuntimeScripts/certification.manifest index 939cdbaefa..50f6943e95 100644 --- a/Ghidra/RuntimeScripts/certification.manifest +++ b/Ghidra/RuntimeScripts/certification.manifest @@ -31,3 +31,4 @@ support/jshellRun||GHIDRA||||END| support/launch.properties||GHIDRA||||END| support/pyghidraRun||GHIDRA||||END| support/sleigh||GHIDRA||||END| +support/updateServerAllowList||GHIDRA||||END| diff --git a/Ghidra/RuntimeScripts/support/analyzeHeadlessREADME.md b/Ghidra/RuntimeScripts/support/analyzeHeadlessREADME.md index d51b0fa17f..a31d6405a1 100644 --- a/Ghidra/RuntimeScripts/support/analyzeHeadlessREADME.md +++ b/Ghidra/RuntimeScripts/support/analyzeHeadlessREADME.md @@ -74,6 +74,7 @@ for common use cases. [-processor <languageID>] [-cspec <compilerSpecID>] [-analysisTimeoutPerFile <timeout in seconds>] + [-allowAllAccess] [-keystore <KeystorePath>] [-connect [<userID>]] [-p] @@ -341,6 +342,19 @@ completed processing prior to timeout will still be saved with the program. Post to detect that analysis has timed out (in Headless processing ONLY) by calling the `getHeadlessAnalysisTimeoutStatus()` method. +### `-allowAllAccess` +When using Ghidra URLs to access a GhidraServer and this option has been omitted, and if server +access has not already disallowed, the stored `Server Allow List` will be updated to allow access. +If access has previously been disallowed, the analyze headless process will fail. + +The use of the `Server Allow List` can be bypassed by including the `-allowAllAccess` option. +Alternatively, the `updateServerAllowList` command within the Ghidra installation may be used to examine +and update the server allow list. Running the `updateServerAllowList` with no argument will provide +usage details. + +In general, there should be no need to deal with this server access concern unless access was +previously disallowed or other Ghidra URLs are obtained and utilized from other sources. + ### `-keystore ` When connecting to a Ghidra Server using PKI or SSH authentication, this option allows specification of a suitable private keystore file. The keystore file should always be properly @@ -1033,6 +1047,7 @@ Below are some general guidelines for wildcard usage: [processor]: #-processor-languageid [cspec]: #-cspec-compilerspecid [timeout]: #-analysistimeoutperfile-timeout-in-seconds +[-allowAllAccess]: #-allowAllAccess [keystore]: #-keystore-keystorepath [connect]: #-connect-userid [password]: #-p diff --git a/Ghidra/RuntimeScripts/support/updateServerAllowList b/Ghidra/RuntimeScripts/support/updateServerAllowList new file mode 100755 index 0000000000..892c537422 --- /dev/null +++ b/Ghidra/RuntimeScripts/support/updateServerAllowList @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +#---------------------------------------------------------------------- +# Update Server Allow List launch +#---------------------------------------------------------------------- + +MAXMEM=128M + +# Launch mode can be changed to one of the following: fg, debug, debug-suspend +LAUNCH_MODE=fg + +# Set the debug address to listen on. +# NOTE: This variable is ignored if not launching in a debugging mode. +DEBUG_ADDRESS=127.0.0.1:13010 + +# Resolve symbolic link if present and get the directory this script lives in. +# NOTE: "readlink -f" is best but works on Linux only, "readlink" will only work if your PWD +# contains the link you are calling (which is the best we can do on macOS), and the "echo" is the +# fallback, which doesn't attempt to do anything with links. +SCRIPT_FILE="$(readlink -f "$0" 2>/dev/null || readlink "$0" 2>/dev/null || echo "$0")" +SCRIPT_DIR="${SCRIPT_FILE%/*}" + +# Set required VMARGS for jar builder application +APP_VMARGS="-DUpdateServerAllowList.Name=$(basename "${SCRIPT_FILE}")" + +# Launch UpdateServerAllowList. +# DEBUG_ADDRESS set via environment for launch.sh +DEBUG_ADDRESS=${DEBUG_ADDRESS} "${SCRIPT_DIR}"/launch.sh "${LAUNCH_MODE}" jdk updateServerAllowList "${MAXMEM}" "${APP_VMARGS}" ghidra.app.util.headless.UpdateServerAllowList "$@" diff --git a/Ghidra/RuntimeScripts/support/updateServerAllowList.bat b/Ghidra/RuntimeScripts/support/updateServerAllowList.bat new file mode 100644 index 0000000000..bb72062030 --- /dev/null +++ b/Ghidra/RuntimeScripts/support/updateServerAllowList.bat @@ -0,0 +1,26 @@ +:: ### +:: IP: GHIDRA +:: +:: Licensed under the Apache License, Version 2.0 (the "License"); +:: you may not use this file except in compliance with the License. +:: You may obtain a copy of the License at +:: +:: http://www.apache.org/licenses/LICENSE-2.0 +:: +:: Unless required by applicable law or agreed to in writing, software +:: distributed under the License is distributed on an "AS IS" BASIS, +:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +:: See the License for the specific language governing permissions and +:: limitations under the License. +:: ## +:: Update Server Allow List launch + +@echo off +setlocal + +:: maximum heap memory may be change if inadequate +set MAXMEM=128M + +set APP_VMARGS=-DUpdateServerAllowList.Name=%~n0 + +call "%~dp0launch.bat" fg jdk updateServerAllowList "%MAXMEM%" "%APP_VMARGS%" ghidra.app.util.headless.UpdateServerAllowList %* diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java index 1c2ecba261..bca9b085a6 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java @@ -15,31 +15,21 @@ */ package ghidra.framework.main; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.io.File; import java.net.URL; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import docking.DialogComponentProvider; -import docking.test.AbstractDockingTest; import ghidra.app.services.CodeViewerService; import ghidra.app.services.ProgramManager; +import ghidra.framework.client.*; import ghidra.framework.data.DomainFileProxy; -import ghidra.framework.model.DomainFolder; -import ghidra.framework.model.Project; -import ghidra.framework.model.ProjectLocator; -import ghidra.framework.model.ToolAssociationInfo; -import ghidra.framework.model.ToolServices; -import ghidra.framework.model.ToolTemplate; +import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.protocol.ghidra.Handler; @@ -48,14 +38,10 @@ import ghidra.program.database.ProgramDB; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.listing.Program; -import ghidra.program.model.symbol.Namespace; -import ghidra.program.model.symbol.SourceType; -import ghidra.program.model.symbol.SymbolTable; +import ghidra.program.model.symbol.*; import ghidra.program.util.ProgramLocation; import ghidra.server.remote.ServerTestUtil; -import ghidra.test.AbstractGhidraHeadedIntegrationTest; -import ghidra.test.TestEnv; -import ghidra.test.ToyProgramBuilder; +import ghidra.test.*; import ghidra.util.exception.AssertException; import ghidra.util.task.TaskMonitor; import utilities.util.FileUtilities; @@ -77,10 +63,16 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { private static final String REPO_NAME = "Test"; private URL remoteFileUrl; + private boolean allowNextServerAccess; @Before public void setUp() throws Exception { + ClientUtil.setAllowListProvider(new MyAllowListProvider()); + UrlAllowListManager.alwaysAllowLocalAccess = false; + UrlAllowListManager.clearAll(); + allowNextServerAccess = true; + env = new TestEnv(); // NOTE: Use of tool templates requires active front-end tool @@ -143,10 +135,8 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { AtomicReference ref = new AtomicReference<>(); runSwing(() -> { - boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); ToolServices toolServices = project.getToolServices(); ref.set(toolServices.launchDefaultToolWithURL(url)); - AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); }); verifyLaunch(ref.get()); @@ -162,10 +152,8 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { AtomicReference ref = new AtomicReference<>(); runSwing(() -> { - boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); ToolServices toolServices = project.getToolServices(); ref.set(toolServices.launchToolWithURL(DEFAULT_TEST_TOOL_NAME, url)); - AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); }); verifyLaunch(ref.get()); @@ -181,9 +169,13 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { URL url = GhidraURL.makeURL(projectLocator, FOLDER, null); - ToolServices toolServices = project.getToolServices(); - PluginTool tool = toolServices.launchDefaultToolWithURL(url); - assertNull(tool); + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchDefaultToolWithURL(url)); + }); + + assertNull(ref.get()); // verify no tool launched DialogComponentProvider dlg = waitForDialogComponent("Unsupported Content"); assertNotNull("Error dialog expected", dlg); @@ -200,9 +192,13 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { URL url = GhidraURL.makeURL(projectLocator, "/x/y", null); - ToolServices toolServices = project.getToolServices(); - PluginTool tool = toolServices.launchDefaultToolWithURL(url); - assertNull(tool); + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchDefaultToolWithURL(url)); + }); + + assertNull(ref.get()); // verify no tool launched DialogComponentProvider dlg = waitForDialogComponent("Content Not Found"); assertNotNull("Error dialog expected", dlg); @@ -216,12 +212,26 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { Project project = env.getProject(); setupDefaultTestTool(project); + allowNextServerAccess = false; // deny first attempt + AtomicReference ref = new AtomicReference<>(); runSwing(() -> { - boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); ToolServices toolServices = project.getToolServices(); ref.set(toolServices.launchDefaultToolWithURL(remoteFileUrl)); - AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); + }); + + assertNull(ref.get()); // verify no tool launched + + DialogComponentProvider dlg = waitForDialogComponent("Content Access Failure"); + runSwing(() -> dlg.close()); + + //Clear allow list cache and allow next attempt + UrlAllowListManager.clearAll(); + allowNextServerAccess = true; + + runSwing(() -> { + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchDefaultToolWithURL(remoteFileUrl)); }); verifyLaunch(ref.get()); @@ -236,10 +246,8 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { AtomicReference ref = new AtomicReference<>(); runSwing(() -> { - boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); ToolServices toolServices = project.getToolServices(); ref.set(toolServices.launchToolWithURL(DEFAULT_TEST_TOOL_NAME, remoteFileUrl)); - AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); }); verifyLaunch(ref.get()); @@ -255,9 +263,13 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { URL badUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST, ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER); - ToolServices toolServices = project.getToolServices(); - PluginTool tool = toolServices.launchDefaultToolWithURL(badUrl); - assertNull(tool); + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchDefaultToolWithURL(badUrl)); + }); + + assertNull(ref.get()); // verify no tool launched DialogComponentProvider dlg = waitForDialogComponent("Unsupported Content"); assertNotNull("Error dialog expected", dlg); @@ -274,9 +286,13 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { URL badUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST, ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, "x", REF); - ToolServices toolServices = project.getToolServices(); - PluginTool tool = toolServices.launchDefaultToolWithURL(badUrl); - assertNull(tool); + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchDefaultToolWithURL(badUrl)); + }); + + assertNull(ref.get()); // verify no tool launched DialogComponentProvider dlg = waitForDialogComponent("Content Not Found"); assertNotNull("Error dialog expected", dlg); @@ -356,4 +372,19 @@ public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { } + private class MyAllowListProvider extends AbstractUrlAllowListProvider { + + @Override + public boolean isAllowed(URL url) { + Boolean allowed = accessAllowed(url); + if (allowed != null) { + return allowed; + } + allowed = allowNextServerAccess; + UrlAllowListManager.updateAccess(url, allowed); + return allowed; + } + + } + } diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java index 53eb70481b..cf5dc7753b 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java @@ -15,33 +15,14 @@ */ package ghidra.server.remote; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.NetworkInterface; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.URL; -import java.net.UnknownHostException; +import java.io.*; +import java.net.*; import java.nio.file.Files; import java.nio.file.Path; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.security.KeyStore.PrivateKeyEntry; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.function.Consumer; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -53,38 +34,24 @@ import org.apache.commons.lang3.RandomStringUtils; import db.buffers.DataBuffer; import generic.hash.HashUtilities; -import generic.test.AbstractGenericTest; -import generic.test.ConcurrentTestExceptionHandler; -import generic.test.TestUtils; +import generic.test.*; import ghidra.framework.Application; -import ghidra.framework.client.ClientUtil; -import ghidra.framework.client.NotConnectedException; -import ghidra.framework.client.RepositoryServerAdapter; +import ghidra.framework.client.*; import ghidra.framework.data.ContentHandler; import ghidra.framework.data.DomainObjectAdapter; -import ghidra.framework.protocol.ghidra.GhidraURL; -import ghidra.framework.protocol.ghidra.Handler; +import ghidra.framework.protocol.ghidra.*; import ghidra.framework.remote.GhidraServerHandle; import ghidra.framework.remote.RMIServerPortFactory; import ghidra.framework.store.FileSystem; import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFolderItem; -import ghidra.net.DefaultKeyManagerFactory; -import ghidra.net.DefaultSSLContextInitializer; -import ghidra.net.DefaultTrustManagerFactory; -import ghidra.net.PKITestUtils; -import ghidra.net.PKIUtils; +import ghidra.net.*; import ghidra.program.model.listing.Program; import ghidra.server.ServerAdmin; import ghidra.server.UserManager; import ghidra.test.ToyProgramBuilder; -import ghidra.util.InvalidNameException; -import ghidra.util.Msg; -import ghidra.util.NamingUtilities; -import ghidra.util.SystemUtilities; -import ghidra.util.exception.AssertException; -import ghidra.util.exception.CancelledException; -import ghidra.util.exception.DuplicateFileException; +import ghidra.util.*; +import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; import ghidra.util.timer.GTimer; import utilities.util.FileUtilities; @@ -662,6 +629,8 @@ public class ServerTestUtil { System.clearProperty(DefaultTrustManagerFactory.GHIDRA_CACERTS_PATH_PROPERTY); + TransientProjectManager.getTransientProjectManager().dispose(); + if (serverProcess != null) { cmdOut.dispose(); diff --git a/GhidraDocs/GhidraClass/Beginner/Introduction_to_Ghidra_Student_Guide.html b/GhidraDocs/GhidraClass/Beginner/Introduction_to_Ghidra_Student_Guide.html index b4972738df..4fc0d4db85 100644 --- a/GhidraDocs/GhidraClass/Beginner/Introduction_to_Ghidra_Student_Guide.html +++ b/GhidraDocs/GhidraClass/Beginner/Introduction_to_Ghidra_Student_Guide.html @@ -1297,10 +1297,10 @@ Before you can do anything else, you must first create a project. Projects are u - Types of annotations:
- Address
-- Execute
-- Program
-- Symbol
-- URL
+- Program
+- Symbol
+- HTTP-URL
+- GHIDRA-URL
@@ -1309,14 +1309,14 @@ Before you can do anything else, you must first create a project. Projects are udiff --git a/GhidraDocs/GhidraClass/Intermediate/HeadlessAnalyzer.html b/GhidraDocs/GhidraClass/Intermediate/HeadlessAnalyzer.html index 19107e51e9..289315b656 100644 --- a/GhidraDocs/GhidraClass/Intermediate/HeadlessAnalyzer.html +++ b/GhidraDocs/GhidraClass/Intermediate/HeadlessAnalyzer.html @@ -39,7 +39,7 @@ [-processor <languageID>] [-cspec <compilerSpecID>] [-analysisTimeoutPerFile <timeout in seconds>] [-keystore <KeystorePath>] [-connect [<userID>]] - [-p] [-commit ["<comment>"]] [-okToDelete] + [-p] [-commit ["<comment>"]] [-okToDelete] [-allowAllAccess] [-max-cpu <max cpu cores to use>] [-loader <desired loader name>] @@ -419,6 +419,22 @@ +
- Annotations To add comment annotations using the comment editor, use the pull-down menu, choose an annotation type, and then click the Add Annotation button.
-
- - +- Address Fill in the interesting address. The comment will display the given address as a hyperlink. Double-click the link to navigate to that address.
-- Execute Fill in path to an executable. Double-click the resulting link to launch the specified executable in your OS with given optional parameters.
-- Program Fill in existing Ghidra program name (case-sensitive) to displays a hyperlink to the given Ghidra program name. Double-click the link to open the program in a new Listing tab. Optionally use the @symbol name after the program name to go to a specific symbol in the program.
-- Symbol Fill in the address of the interesting symbol. The comment will display the given symbol as a hyperlink. Double-click to navigate to the symbol. Changes to the symbol in the Listing will automatically change the comment to display the new symbol.
-- URL Displays the given URL as a hyperlink. Double-click to open what the link is pointing to. References to ghidra://, which refer to a program within a Ghidra Server repository, will be opened within the Listing display, while all other URL protocols (e.g., http://, https://, file://, etc.) will be launched via an external web browser. See HELP command configuration for Processor Manuals for more information.
-- Address Fill in the interesting address. The comment will display the given address as a hyperlink. Double-click the link to navigate to that address.
+- Program Fill in existing Ghidra program name (case-sensitive) to displays a hyperlink to the given Ghidra program name. Double-click the link to open the program in a new Listing tab. Optionally use the @symbol name after the program name to go to a specific symbol in the program.
+- Symbol Fill in the address of the interesting symbol. The comment will display the given symbol as a hyperlink. Double-click to navigate to the symbol. Changes to the symbol in the Listing will automatically change the comment to display the new symbol.
+- HTTP-URL Displays the given URL as a hyperlink. Double-click to open what the link is pointing to + using your configured web browser. See HELP command configuration for Processor Manuals for more information.
+- GHIDRA-URL References a local Ghidra project file or a remote Ghidra Server repository file. + The referenced Program file will be opened within the Listing display.
++ +-allowAllAccess +
++
+- Access to remote servers may be subject to a Server Allow List. In order to avoid user + prompting, connections to non-localhost servers, not previously added to the list via the GUI or + support/updateServerAllowList shell script, will fail by default. + Execute this script without arguments to see usage information. + Access denial may be avoided by either updating the Server Allow List or including the + -allowAllAccess command line option. + NOTE: Server Allow List screening is independent of possible SSL/TLS server authentication + which may be required. +
+-max-cpu <max cpu cores to use>