Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-37566] - Always run setAccessible and classloader constructors in the privileged blocks #118

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/main/java/hudson/remoting/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.jenkinsci.remoting.util.IOUtils;
import org.jenkinsci.remoting.util.ReflectionUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
Expand Down Expand Up @@ -78,16 +79,16 @@
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateException;
import java.security.NoSuchAlgorithmException;
import java.security.KeyManagementException;
import java.security.SecureRandom;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -138,8 +139,8 @@ public void setTextMode(boolean b) {
@Option(name="-cp",aliases="-classpath",metaVar="PATH",
usage="add the given classpath elements to the system classloader.")
public void addClasspath(String pathList) throws Exception {
Method $addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
$addURL.setAccessible(true);
final Method $addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
ReflectionUtils.makeAccessible($addURL);

for(String token : pathList.split(File.pathSeparator))
$addURL.invoke(ClassLoader.getSystemClassLoader(),new File(token).toURI().toURL());
Expand Down
50 changes: 42 additions & 8 deletions src/main/java/hudson/remoting/RemoteClassLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
import java.net.URL;
import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -46,6 +49,7 @@
import java.util.logging.Logger;

import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
import org.jenkinsci.remoting.util.ReflectionUtils;

import javax.annotation.CheckForNull;

Expand Down Expand Up @@ -105,14 +109,29 @@ final class RemoteClassLoader extends URLClassLoader {
*/
private final Map<String,ClassReference> prefetchedClasses = Collections.synchronizedMap(new HashMap<String,ClassReference>());

public static ClassLoader create(ClassLoader parent, IClassLoader proxy) {
public static ClassLoader create(final ClassLoader parent, final IClassLoader proxy) {
if(proxy instanceof ClassLoaderProxy) {
// when the remote sends 'RemoteIClassLoader' as the proxy, on this side we get it
// as ClassLoaderProxy. This means, the so-called remote classloader here is
// actually our classloader that we exported to the other side.
return ((ClassLoaderProxy)proxy).cl;
}
return new RemoteClassLoader(parent, proxy);

// Create new RemoteClassLoader otherwise
final RemoteClassLoader created;
try {
created = AccessController.doPrivileged(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should only ever need to use this in case you are running with a security manager enabled, which for Remoting we are not. So I do not see the point of this PR. If it is just to get SpotBugs to shut up, use the warning suppression annotation, or just configure a SpotBugs exclude for that warning in the POM.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And there is a Java JEP proposing to deprecate the security manager, and associated methods like this.

new PrivilegedExceptionAction<RemoteClassLoader>() {
@Override
public RemoteClassLoader run() throws Exception {
return new RemoteClassLoader(parent, proxy);
}
});
} catch (PrivilegedActionException ex) {
LOGGER.log(SEVERE, "Cannot instantiate the Remote classloader", ex);
throw new IllegalStateException( "Cannot instantiate the Remote classloader", ex);
}
return created;
}

private RemoteClassLoader(ClassLoader parent, IClassLoader proxy) {
Expand Down Expand Up @@ -327,7 +346,7 @@ ClassReference toRef(ClassFile2 cf) {
static {
try {
gCLL = ClassLoader.class.getDeclaredMethod("getClassLoadingLock", String.class);
gCLL.setAccessible(true);
ReflectionUtils.makeAccessible(gCLL);
} catch (NoSuchMethodException x) {
// OK, Java 6
} catch (Exception x) {
Expand Down Expand Up @@ -993,12 +1012,27 @@ public String toString() {
* any new classpath. In this way, we can effectively use this classloader as a representation
* of the bootstrap classloader.
*/
private static final ClassLoader PSEUDO_BOOTSTRAP = new URLClassLoader(new URL[0],(ClassLoader)null) {
@Override
public String toString() {
return "PSEUDO_BOOTSTRAP";
private static final ClassLoader PSEUDO_BOOTSTRAP;

static {
try {
PSEUDO_BOOTSTRAP = AccessController.doPrivileged(
new PrivilegedExceptionAction<ClassLoader>() {
@Override
public ClassLoader run() throws Exception {
return new URLClassLoader(new URL[0],(ClassLoader)null) {
@Override
public String toString() {
return "PSEUDO_BOOTSTRAP";
}
};
}
});
} catch (PrivilegedActionException ex) {
LOGGER.log(SEVERE, "Cannot instantiate the Pseudo Bootstrap classloader", ex);
throw new IllegalStateException( "Cannot instantiate the Pseudo Bootstrap classloader", ex);
}
};
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/hudson/remoting/RemoteInvocationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.jenkinsci.remoting.RoleChecker;
import org.jenkinsci.remoting.util.ReflectionUtils;

/**
* Sits behind a proxy object and implements the proxy logic.
Expand Down Expand Up @@ -884,7 +885,7 @@ protected Serializable perform(Channel channel) throws Throwable {
Method m = choose(clazz);
if(m==null)
throw new IllegalStateException("Unable to call " + methodName + ". No matching method found in " + Arrays.toString(clazz) + " for " + o);
m.setAccessible(true); // in case the class is not public
ReflectionUtils.makeAccessible(m);
Object r;
try {
r = m.invoke(o, arguments);
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/hudson/remoting/Which.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
package hudson.remoting;

import org.jenkinsci.remoting.util.ReflectionUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
Expand All @@ -33,6 +35,7 @@
import java.net.URLConnection;
import java.net.JarURLConnection;
import java.lang.reflect.Field;
import java.security.PrivilegedActionException;
import java.util.zip.ZipFile;
import java.util.jar.JarFile;
import java.util.logging.Logger;
Expand Down Expand Up @@ -141,20 +144,21 @@ public static File jarFile(Class clazz) throws IOException {
Object delegate = is;
while (delegate.getClass().getEnclosingClass()!=ZipFile.class) {
Field f = delegate.getClass().getDeclaredField("delegate");
f.setAccessible(true);
ReflectionUtils.makeAccessible(f);

delegate = f.get(delegate);
//JENKINS-5922 - workaround for CertificateReaderInputStream; JBoss 5.0.0, EAP 5.0 and EAP 5.1
if(delegate.getClass().getName().equals("java.util.jar.JarVerifier$VerifierStream")){
f = delegate.getClass().getDeclaredField("is");
f.setAccessible(true);
ReflectionUtils.makeAccessible(f);
delegate = f.get(delegate);
}
}
Field f = delegate.getClass().getDeclaredField("this$0");
f.setAccessible(true);
ReflectionUtils.makeAccessible(f);
ZipFile zipFile = (ZipFile)f.get(delegate);
return new File(zipFile.getName());
} catch (NoSuchFieldException e) {
} catch (NoSuchFieldException | PrivilegedActionException e) {
// something must have changed in JBoss5. fall through
LOGGER.log(Level.FINE, "Failed to resolve vfszip into a jar location",e);
} catch (IllegalAccessException e) {
Expand Down Expand Up @@ -201,8 +205,9 @@ public static File jarFile(Class clazz) throws IOException {
// so this just keeps getting tricker and trickier...
try {
Field f = ZipFile.class.getDeclaredField("name");
f.setAccessible(true);
return new File((String) f.get(jarFile));
if (ReflectionUtils.makeAccessibleOrLog(f, LOGGER, Level.FINE)) {
return new File((String) f.get(jarFile));
}
} catch (NoSuchFieldException e) {
LOGGER.log(Level.INFO, "Failed to obtain the local cache file name of "+resURL, e);
} catch (IllegalAccessException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jenkinsci.remoting.nio;

import org.jenkinsci.remoting.util.ReflectionUtils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
Expand All @@ -20,6 +22,7 @@
import java.nio.channels.SelectableChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.security.PrivilegedActionException;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -47,12 +50,12 @@ protected FileInputStream unwrap(InputStream i) {
if (i instanceof BufferedInputStream) {
try {
Field $in = FilterInputStream.class.getDeclaredField("in");
$in.setAccessible(true);
ReflectionUtils.makeAccessible($in);
return unwrap((InputStream)$in.get(i));
} catch (NoSuchFieldException e) {
warn(e);
return null;
} catch (IllegalAccessException e) {
} catch (IllegalAccessException | PrivilegedActionException e) {
warn(e);
return null;
}
Expand All @@ -67,12 +70,12 @@ protected FileOutputStream unwrap(OutputStream i) {
if (i instanceof BufferedOutputStream) {
try {
Field $in = FilterOutputStream.class.getDeclaredField("out");
$in.setAccessible(true);
ReflectionUtils.makeAccessible($in);
return unwrap((OutputStream)$in.get(i));
} catch (NoSuchFieldException e) {
warn(e);
return null;
} catch (IllegalAccessException e) {
} catch (IllegalAccessException | PrivilegedActionException e) {
warn(e);
return null;
}
Expand Down Expand Up @@ -111,11 +114,11 @@ public SocketChannel create(FileDescriptor fd) {
FileDescriptor.class,
InetSocketAddress.class
);
$c.setAccessible(true);
ReflectionUtils.makeAccessible($c);

// increment the FileDescriptor use count since we are giving it to SocketChannel
Method $m = fd.getClass().getDeclaredMethod("incrementAndGetUseCount");
$m.setAccessible(true);
ReflectionUtils.makeAccessible($m);
$m.invoke(fd);

return (SocketChannel)$c.newInstance(SelectorProvider.provider(), fd, null);
Expand All @@ -128,7 +131,7 @@ public SocketChannel create(FileDescriptor fd) {
} catch (ClassNotFoundException e) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-catch around should be cleaned up during the refactoring

warn(e);
return null;
} catch (IllegalAccessException e) {
} catch (IllegalAccessException | PrivilegedActionException e) {
warn(e);
return null;
} catch (InvocationTargetException e) {
Expand Down
95 changes: 95 additions & 0 deletions src/main/java/org/jenkinsci/remoting/util/ReflectionUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* The MIT License
*
* Copyright (c) 2016, Oleg Nenashev, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jenkinsci.remoting.util;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import java.lang.reflect.AccessibleObject;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Provides some reflection util methods.
* This class is not designed for the external use.
* @author Oleg Nenashev
*/
@Restricted(NoExternalUse.class)
public class ReflectionUtils {

private ReflectionUtils() {
// Instantiation is prohibited
}

/**
* Makes a method accessible using reflection.
* There is no automatic accessibility status recovery.
* @param object Object to be modified
* @throws PrivilegedActionException Missing permissions or other modification error.
* This exception cannot happen if the object is accessible when the method is invoked.
*/
public static void makeAccessible(final @Nonnull AccessibleObject object) throws PrivilegedActionException {
Copy link
Member

@kohsuke kohsuke Oct 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 This is a wrong usage of AccessController.doPrivileged() that creates a security hole. It allows untrusted caller to use this public method to elevate privilege and make any method/field accessible.

It's best not to fiddle with AcessController IMO. The only situation where it makes a difference is when remoting is in trusted code source and the client app code is not trusted. Remoting deals too much with classloader and other potentially dangerous code. It's best just not to let the untrusted caller use remoting at all, which can be achieved by not using AccessController.doPrivileged()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trusted caller can always do it via the reflection API, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, restriction of this Utility class access is reasonable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trusted caller can always do it via the reflection API, no?

I'm not sure how to interpret that.

A decade ago, I worked on JAXB, which eventually became a part of JavaSE. A part of that integration work involves making JAXB safely usable from untrusted code (think of applet/JavaFX, for example), and that experience taught me a lot about the danger and difficulty of correctly using privilege escalation.

The findbugs explanation of DP_DO_INSIDE_DO_PRIVILEGED does not discuss any implication of privilege escalations and down right dangerous.

IIUC, the motivation here is to make this module FindBugs clean, and in that case I recommend simply disabling this detector.

I can tell you more about this if you really care about, but it's much too long to type it in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I submitted it as a separate PR. I went through some docs/discussions on this topic and decided it may be reasonable to perform such change.

The motivation is to really make FindBugs clean, but I agree it requires more investigation. Putting it on hold

if (object.isAccessible()) {
// No need to change anything
return;
}
AccessController.doPrivileged(
new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws Exception {
object.setAccessible(true);
return null;
}
});
}


/**
* Makes a method accessible using reflection.
* In the case of {@link PrivilegedActionException} logs them without propagating exception.
* There is no automatic accessibility status recovery.
* @param object Object to be modified
* @param logger Logger for errors
* @param level Logging level for errors
* @return {@code true} on success path, {@code false} if the modification error happens
*/
@CheckReturnValue
public static boolean makeAccessibleOrLog(final @Nonnull AccessibleObject object,
final @Nonnull Logger logger,
final @Nonnull Level level) {
try {
makeAccessible(object);
} catch (PrivilegedActionException ex) {
logger.log(level, String.format("Cannot make the field %s accessible", object), ex);
return false;
}
return true;
}

}
Loading