mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-27 23:17:03 +08:00
GP-6832 Resolved various Ghidra Server security concerns
This commit is contained in:
@@ -351,12 +351,22 @@ public class Repository implements FileSystemListener, RepositoryLogger {
|
||||
validate();
|
||||
validateAdminPrivilege(currentUser);
|
||||
|
||||
Set<String> allUsers = Set.of(mgr.getAllUsers(currentUser));
|
||||
Set<String> updatedUsers = new HashSet<>();
|
||||
|
||||
LinkedHashMap<String, User> newUserMap = new LinkedHashMap<>();
|
||||
for (User user : users) {
|
||||
String userName = user.getName();
|
||||
if (UserManager.ANONYMOUS_USERNAME.equals(userName)) {
|
||||
if (UserManager.ANONYMOUS_USERNAME.equals(userName) ||
|
||||
!allUsers.contains(userName)) {
|
||||
continue; // ignore
|
||||
}
|
||||
if (!user.hasWritePermission() && !user.isReadOnly() && !user.isAdmin()) {
|
||||
throw new IOException("User specified with invalid permission: " + userName);
|
||||
}
|
||||
if (!updatedUsers.add(userName)) {
|
||||
throw new IOException("Duplicate user entry specified: " + userName);
|
||||
}
|
||||
newUserMap.put(userName, user);
|
||||
}
|
||||
User user = newUserMap.get(currentUser);
|
||||
@@ -508,7 +518,8 @@ public class Repository implements FileSystemListener, RepositoryLogger {
|
||||
* @throws UserAccessException if currentUser does not have admin priviledge
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
private void writeUserList(LinkedHashMap<String, User> newUserMap, boolean allowAnonymous)
|
||||
private synchronized void writeUserList(LinkedHashMap<String, User> newUserMap,
|
||||
boolean allowAnonymous)
|
||||
throws IOException {
|
||||
|
||||
File temp = new File(userAccessFile.getParentFile(), "tempAccess.tmp");
|
||||
|
||||
@@ -751,10 +751,10 @@ public class UserManager {
|
||||
}
|
||||
|
||||
/*
|
||||
* Regex: matches if the entire string is alpha, digit, ".", "-", "_", fwd or back slash.
|
||||
* Regex: matches if the entire string is alpha, digit, ".", "-", "_".
|
||||
*/
|
||||
private static final Pattern VALID_USERNAME_REGEX =
|
||||
Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9.\\-_/\\\\]*");
|
||||
Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9.\\-_]*");
|
||||
|
||||
/**
|
||||
* Ensures a name only contains valid characters.
|
||||
|
||||
+1
-1
@@ -718,7 +718,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject
|
||||
public void terminateCheckout(String parentPath, String itemName, long checkoutId,
|
||||
boolean notify) throws IOException {
|
||||
synchronized (syncObject) {
|
||||
validate(); // relax read-only restriction
|
||||
validate();
|
||||
try {
|
||||
RepositoryFile rf = getFile(parentPath, itemName);
|
||||
if (rf != null) {
|
||||
|
||||
+5
-26
@@ -39,10 +39,8 @@ import ghidra.server.remote.RemoteLoggingUtil;
|
||||
* use of a dual-signed token.
|
||||
*/
|
||||
public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
static final Logger log = LogManager.getLogger(PKIAuthenticationModule.class);
|
||||
|
||||
private static final long MAX_TOKEN_TIME = 5 * 60000; // 5-minutes
|
||||
private static final int TOKEN_SIZE = 64;
|
||||
static final Logger log = LogManager.getLogger(PKIAuthenticationModule.class);
|
||||
|
||||
private X500Principal[] authorities; // imposed on client certificate
|
||||
private boolean anonymousAllowed;
|
||||
@@ -70,7 +68,7 @@ public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
public Callback[] getAuthenticationCallbacks() {
|
||||
SignatureCallback sigCb;
|
||||
try {
|
||||
byte[] token = TokenGenerator.getNewToken(TOKEN_SIZE);
|
||||
byte[] token = TokenGenerator.getNewToken();
|
||||
boolean usingSelfSignedCert =
|
||||
DefaultKeyManagerFactory.usingGeneratedSelfSignedCertificate();
|
||||
SignedToken signedToken = DefaultKeyManagerFactory
|
||||
@@ -88,27 +86,6 @@ public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void checkTokenIntegrity(byte[] token) throws LoginException {
|
||||
if (token.length != TOKEN_SIZE) {
|
||||
throw new FailedLoginException("Invalid Signature callback");
|
||||
}
|
||||
|
||||
boolean isZeroToken = true;
|
||||
for (byte b : token) {
|
||||
if (b != 0) {
|
||||
isZeroToken = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isZeroToken) {
|
||||
throw new FailedLoginException("Invalid Signature callback");
|
||||
}
|
||||
|
||||
if (!TokenGenerator.isRecentToken(token, MAX_TOKEN_TIME)) {
|
||||
throw new FailedLoginException("Stale Signature callback");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.server.security.AuthenticationModule#authenticate(ghidra.server.UserManager, javax.security.auth.Subject, javax.security.auth.callback.Callback[])
|
||||
*/
|
||||
@@ -142,7 +119,9 @@ public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
try {
|
||||
|
||||
byte[] token = sigCb.getToken();
|
||||
checkTokenIntegrity(token);
|
||||
if (!TokenGenerator.isValidToken(token)) {
|
||||
throw new FailedLoginException("Stale Signature callback");
|
||||
}
|
||||
|
||||
boolean usingSelfSignedCert =
|
||||
DefaultKeyManagerFactory.usingGeneratedSelfSignedCertificate();
|
||||
|
||||
+3
-4
@@ -45,8 +45,7 @@ import ghidra.server.UserManager;
|
||||
*/
|
||||
public class SSHAuthenticationModule {
|
||||
|
||||
private static final long MAX_TOKEN_TIME = 10000;
|
||||
private static final int TOKEN_SIZE = 64;
|
||||
|
||||
|
||||
private final boolean nameCallbackAllowed;
|
||||
|
||||
@@ -72,7 +71,7 @@ public class SSHAuthenticationModule {
|
||||
if (addNameCallback) {
|
||||
list.add(new NameCallback("User ID:"));
|
||||
}
|
||||
byte[] token = TokenGenerator.getNewToken(TOKEN_SIZE);
|
||||
byte[] token = TokenGenerator.getNewToken();
|
||||
try {
|
||||
boolean usingSelfSignedCert =
|
||||
DefaultKeyManagerFactory.usingGeneratedSelfSignedCertificate();
|
||||
@@ -190,7 +189,7 @@ public class SSHAuthenticationModule {
|
||||
}
|
||||
|
||||
byte[] token = sshCb.getToken();
|
||||
if (!TokenGenerator.isRecentToken(token, MAX_TOKEN_TIME)) {
|
||||
if (!TokenGenerator.isValidToken(token)) {
|
||||
throw new FailedLoginException("Stale SSH Signature callback");
|
||||
}
|
||||
|
||||
|
||||
+66
-9
@@ -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,27 +17,52 @@ package ghidra.server.security;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import generic.random.SecureRandomFactory;
|
||||
|
||||
public class TokenGenerator {
|
||||
|
||||
static byte[] getNewToken(int size) {
|
||||
private static final long MAX_TTL_MS = 60_000; // max token time-to-live 60s
|
||||
|
||||
private static final int TOKEN_SIZE = 64;
|
||||
|
||||
private static CachedTokenSet tokenCache = new CachedTokenSet();
|
||||
|
||||
/**
|
||||
* {@return a single-use token byte sequence with embedded timestamp}
|
||||
*/
|
||||
static byte[] getNewToken() {
|
||||
SecureRandom random = SecureRandomFactory.getSecureRandom();
|
||||
byte[] token = new byte[size - 8];
|
||||
byte[] token = new byte[TOKEN_SIZE - 8];
|
||||
random.nextBytes(token);
|
||||
byte[] stampedToken = new byte[token.length + 8];
|
||||
byte[] stampedToken = new byte[TOKEN_SIZE];
|
||||
System.arraycopy(token, 0, stampedToken, 8, token.length);
|
||||
putLong(stampedToken, 0, (new Date()).getTime());
|
||||
tokenCache.add(stampedToken);
|
||||
return stampedToken;
|
||||
}
|
||||
|
||||
static boolean isRecentToken(byte[] token, long maxTime) {
|
||||
if (token.length < 8) {
|
||||
/**
|
||||
* Determine if the specified token has not yet been consumed and is still valid.
|
||||
* <p>
|
||||
* NOTE: This method may only be invoked once per token after which the token will become
|
||||
* invalid.
|
||||
*
|
||||
* @param token token previously issued
|
||||
* @return true if token is valid and now consumed
|
||||
*/
|
||||
static boolean isValidToken(byte[] token) {
|
||||
if (token.length != TOKEN_SIZE || !tokenCache.consume(token)) {
|
||||
return false;
|
||||
}
|
||||
long diff = (new Date()).getTime() - getLong(token, 0);
|
||||
return (diff >= 0 && diff < maxTime);
|
||||
long issueTime = getLong(token, 0);
|
||||
if (issueTime <= 0) {
|
||||
return false;
|
||||
}
|
||||
long diff = (new Date()).getTime() - issueTime;
|
||||
return (diff >= 0 && diff < MAX_TTL_MS);
|
||||
}
|
||||
|
||||
private static long getLong(byte[] data, int offset) {
|
||||
@@ -59,4 +84,36 @@ public class TokenGenerator {
|
||||
return ++offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link CachedTokenSet} tracks timed token issuance and insures that they remain
|
||||
* valid for one-time consumption within limited life-span.
|
||||
*/
|
||||
private static class CachedTokenSet {
|
||||
|
||||
private final Map<byte[], Long> cache = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
CachedTokenSet() {
|
||||
// Perform token cleanup every 5-seconds
|
||||
scheduler.scheduleAtFixedRate(this::cleanup, 5, 5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
void add(byte[] token) {
|
||||
cache.put(token, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
boolean consume(byte[] value) {
|
||||
Long storedAt = cache.remove(value); // remove on retrieval
|
||||
if (storedAt == null)
|
||||
return false;
|
||||
return (System.currentTimeMillis() - storedAt < MAX_TTL_MS);
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
cache.entrySet().removeIf(e -> now - e.getValue() >= MAX_TTL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -353,6 +353,7 @@ public class RepositoryFile {
|
||||
throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
validate();
|
||||
repository.validateWritePrivilege(user); // don't allow update if read-only
|
||||
folderItem.updateCheckoutVersion(checkoutId, checkoutVersion, user);
|
||||
}
|
||||
}
|
||||
@@ -367,6 +368,7 @@ public class RepositoryFile {
|
||||
public void terminateCheckout(long checkoutId, String user, boolean notify) throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
validate();
|
||||
repository.validateWritePrivilege(user); // don't allow update if read-only
|
||||
ItemCheckoutStatus coStatus = folderItem.getCheckout(checkoutId);
|
||||
if (coStatus != null) {
|
||||
User userObj = repository.getUser(user);
|
||||
|
||||
Reference in New Issue
Block a user