Merge remote-tracking branch

'origin/GP-6796_ghidra1_GhidraURLAllowListInterface' into Ghidra_12.2
This commit is contained in:
ghidra1
2026-09-04 15:52:59 -04:00
39 changed files with 1541 additions and 216 deletions
@@ -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
@@ -22,9 +22,9 @@
hyperlink.</P>
<!-- Annotation Example -->
<P>The following text shows the syntax of a sample URL annotation:</P>
<P>The following text shows the syntax of a sample HTTP URL annotation:</P>
<pre><font size="4"><br>
<b>{@<i>url</i></b> "<i>http://www.google.com</i>"</b> "Search Web"<b>}</b><br>
<b>{@<i>url</i></b> "<i>https://www.google.com</i>"</b> "Search Web"<b>}</b><br>
</font><br></pre>
<P>The bold text is required for all annotations. The italicized text is required but is
@@ -38,7 +38,7 @@
<!-- image -->
<P align="center"><IMG border="0" src="images/CommentDialogURLExample.png" alt=""><BR>
<I>URL Annotation Example</I></P>
<I>HTTP URL Annotation Example</I></P>
<P>The image above shows a URL annotation in its text form as entered into the EOL Comment
tab of the Comments dialog.<BR>
@@ -46,11 +46,13 @@
The image below shows how the annotation is rendered in Ghidra.</P>
<P align="center"><IMG border="2" src="images/RenderedURLExample.png" alt=""><BR>
<I>Rendered URL Annotation Example</I></P>
<I>Rendered HTTP URL Annotation Example</I></P>
<P>When the URL text (e.g., "http://www.google.com") in the above image is clicked from within
<P>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. </P>
<I>GHIDRA URL Annotation Example</I></P>
<P>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:</P>
@@ -64,6 +66,14 @@
<i>ghidra:/[&lt;project-path&gt/]&lt;project-name&gt;?/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i><BR>
Example: <i>ghidra:/share/MyProject?/notepad.exe#entry</i>
</P>
<P><IMG border="0" src="images/warning.help.png" alt="Note"> Clicking on all remote URL annotations will be
will be subject to the <b>Server Allow List</b> resulting in a possible confirmation dialog.
This is independent of possible SSL/TLS server authentication which may be required.
At anytime the <b>Server Allow List</b> may be cleared via the Project window
<b>Edit->Clear Server Allow List...</b> or managed using the <I>support/updateServerAllowList</I> shell script.
Execute this script without arguments to see usage information.</P>
</BLOCKQUOTE>
<H2>Valid Annotations</H2>
@@ -197,7 +207,7 @@
<TD valign="top" width="12%">
<OL style="margin-left: 15px;">
<LI>URL</LI>
<LI>HTTP/GHIDRA URL</LI>
<LI>[display text]<BR>
</LI>
@@ -219,11 +229,11 @@
<TD valign="top" width="25%">
<UL style="margin-left: 10px;">
<LI>{@url "http://www.google.com"}</LI>
<LI>{@url "https://www.google.com"}</LI>
<LI>{@url "http://www.google.com" "google"}</LI>
<LI>{@url "https://www.google.com" "google"}</LI>
<LI>{@url "http://www.google.com" "click here for google"}</LI>
<LI>{@url "https://www.google.com" "click here for google"}</LI>
<LI>{@url "ghidra://myserver/Repo/notepad.exe"}</LI>
@@ -355,6 +365,10 @@
quotes (") around content inside of the annotation tag, excluding the <B>@<I>name</I></B>
part of the tag. Further, some annotations require quotes, as listed in the table above
(e.g., the <B>Execute</B> annotation requires quotes). It is considered good practice to
quote all annotation parameter values.</P><P><IMG border="0" src="images/warning.help.png" alt="Note"> All annotations support double
quotes (") around content inside of the annotation tag, excluding the <B>@<I>name</I></B>
part of the tag. Further, some annotations require quotes, as listed in the table above
(e.g., the <B>Execute</B> annotation requires quotes). It is considered good practice to
quote all annotation parameter values.</P>
@@ -98,8 +98,6 @@
</UL>
</BLOCKQUOTE>
<H3>&nbsp;</H3>
<H3><A name="Manage_Certificates"></A>Manage Certificates (Windows and macOS only)&nbsp;</H3>
<BLOCKQUOTE>
@@ -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.</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG src="help/shared/note.png" border="0"> 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.</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG src="help/shared/note.png" border="0"> Specifying the single user certificate
<P><IMG src="help/shared/note.png" border="0"> Setting your user PKI certificate key store
may also be required if Ghidra communicates with other web services which rely on PKI user
authentication.</P>
<P><IMG src="help/shared/note.png" border="0"> Specifying the single user certificate
keystore in this fashion will prevent the OS managed keystore from being used
(applied to Windows and macOS only).</P>
(applies to Windows and macOS only).</P>
</BLOCKQUOTE>
<H3><A name="Clear_PKI_Certificate"></A>Clear PKI Certificate &nbsp;</H3>
@@ -149,6 +147,32 @@
</BLOCKQUOTE>
<H3><A name="Clear_Server_Allow_List"></A>Server Allow List &nbsp;</H3>
<BLOCKQUOTE>
<P>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 <I>Server Allow List</I>. 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 <I>Server Allow List</I>. Two options exist for altering this list:</P>
<UL>
<LI>To clear the entire Server Allow List from the Ghidra GUI Project Window,
choose <B>Edit<IMG src="help/shared/arrow.gif" border="0">Clear Server Allow List...</B>.</LI>
<LI>From a system command prompt, the <B>updateServerAllowList</B> command
can be used to view and selectively modify the allow-list. This command can
be found within the Ghidra installation <B>support</B> directory.
Server Allow List entries may be displayed with the <I>-list</I>
option, and added or modified using the <I>-allow or -disallow</I> options.
Entries may also removed entirely by using the <I>-clear or -clearAll</I> option.</LI>
</UL>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Exit_Ghidra"></A>Exiting Ghidra</H2>
<BLOCKQUOTE>
@@ -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, "[\"<comment>\"]]"),
OK_TO_DELETE("-okToDelete", false),
ALLOW_ALL_ACCESS("-allowAllAccess", false),
MAX_CPU("-max-cpu", true, "<max cpu cores to use>"),
LIBRARY_SEARCH_PATHS("-librarySearchPaths", true, "<path1>[;<path2>...]"),
LOADER(Loader.COMMAND_LINE_ARG_PREFIX, true, "<desired loader name>"),
@@ -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
}
}
}
@@ -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 {
@@ -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.
@@ -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<ServerSpecification, AccessRecord> accessMap =
UrlAllowListManager.getAccessMap();
List<ServerSpecification> 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 <protocol>://<hostname>:<port>] [-disallow <protocol>://<hostname>:<port>] [-clear <protocol>://<hostname>:<port>] [-clearAll] [-list]\n");
System.err.println(buf.toString());
System.exit(0);
}
}
@@ -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;
}
@@ -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.
* <p>
* 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<String> 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
@@ -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 =
@@ -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 {
@@ -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);
}
}
@@ -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.
* <p>
* 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;
}
}
@@ -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) {}
@@ -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<ServerInfo, RepositoryServerAdapter> 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.
* <p>
* 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.
* <p>
* 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);
@@ -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.
* <p>
* 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;
}
}
@@ -60,6 +60,11 @@ public class RepositoryServerAdapter {
/**
* Construct a repository server interface adapter.
* <p>
* 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 {
@@ -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<ServerSpecification> {
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;
}
}
@@ -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);
}
@@ -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;
}
@@ -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);
}
@@ -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();
}
}
/**
@@ -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() {
@@ -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;
@@ -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) {
@@ -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();
@@ -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()) {
@@ -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();
@@ -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) {
@@ -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 {
@@ -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|
@@ -74,6 +74,7 @@ for common use cases.
[<a href="#-processor-languageid">-processor &lt;languageID&gt;</a>]
[<a href="#-cspec-compilerspecid">-cspec &lt;compilerSpecID&gt;</a>]
[<a href="#-analysistimeoutperfile-timeout-in-seconds">-analysisTimeoutPerFile &lt;timeout in seconds&gt;</a>]
[<a href="#-allowAllAccess">-allowAllAccess</a>]
[<a href="#-keystore-keystorepath">-keystore &lt;KeystorePath&gt;</a>]
[<a href="#-connect-userid">-connect [&lt;userID&gt;]</a>]
[<a href="#-p">-p</a>]
@@ -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 <KeystorePath>`
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
+28
View File
@@ -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 "$@"
@@ -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 %*
@@ -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();
@@ -1297,10 +1297,10 @@ Before you can do anything else, you must first create a project. Projects are u
<li>Types of annotations:</li>
<ul>
<li>Address</li>
<li>Execute</li>
<li>Program</li>
<li>Symbol</li>
<li>URL</li>
<li>Program</li>
<li>Symbol</li>
<li>HTTP-URL</li>
<li>GHIDRA-URL</li>
</ul>
</ul>
<div role="note">
@@ -1309,14 +1309,14 @@ Before you can do anything else, you must first create a project. Projects are u
<ul>
<li><b>Annotations</b> To add comment annotations using the comment editor, use the pull-down menu, choose an annotation type, and then click the <b>Add Annotation</b> button.</li>
<ul>
<li><b>Address</b> Fill in the interesting address. The comment will display the given address as a hyperlink. Double-click the link to navigate to that address.</li>
<li><b>Execute</b> Fill in path to an executable. Double-click the resulting link to launch the specified executable in your OS with given optional parameters.</li>
<li><b>Program</b> 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.</li>
<li><b>Symbol</b> 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.</li>
<li><b>URL</b> 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.</li>
</ul>
<li><b>Address</b> Fill in the interesting address. The comment will display the given address as a hyperlink. Double-click the link to navigate to that address.</li>
<li><b>Program</b> 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.</li>
<li><b>Symbol</b> 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.</li>
<li><b>HTTP-URL</b> 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.</li>
<li><b>GHIDRA-URL</b> References a local Ghidra project file or a remote Ghidra Server repository file.
The referenced Program file will be opened within the Listing display.</li>
</ul>
</ul>
</p>
</div>
@@ -39,7 +39,7 @@
[-processor &lt;languageID&gt;] [-cspec &lt;compilerSpecID&gt;]
[-analysisTimeoutPerFile &lt;timeout in seconds&gt;]
[-keystore &lt;KeystorePath&gt;] [-connect [&lt;userID&gt;]]
[-p] [-commit [&quot;&lt;comment&gt;&quot;]] [-okToDelete]
[-p] [-commit [&quot;&lt;comment&gt;&quot;]] [-okToDelete] [-allowAllAccess]
[-max-cpu &lt;max cpu cores to use&gt;] [-loader &lt;desired loader name&gt;]
</pre>
</span>
@@ -419,6 +419,22 @@
</ul>
</section>
<section>
<header><span style="color:#FFD700">-allowAllAccess</span></header>
<br><br>
<ul>
<li>Access to remote servers may be subject to a <b>Server Allow List</b>. In order to avoid user
prompting, connections to non-localhost servers, not previously added to the list via the GUI or
<I>support/updateServerAllowList</I> shell script, will fail by default.
Execute this script without arguments to see usage information.
Access denial may be avoided by either updating the <b>Server Allow List</b> or including the
<span style="color:#FFD700">-allowAllAccess</span> command line option.
NOTE: <b>Server Allow List</b> screening is independent of possible SSL/TLS server authentication
which may be required.
</li>
</ul>
</section>
<section>
<header><span style="font-size:40px;color:#FFD700">-max-cpu &lt;max cpu cores to use&gt;</span></header>
<br><br><br>