提交 021912b9 编写于 作者: J Jesse Glick

Merge branch 'master' into AccessDeniedException2-header-size-JENKINS-39402

......@@ -56,9 +56,14 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=>
</ul>
</div><!--=TRUNK-END=-->
<h3><a name=v2.44>What's new in 2.44</a> (2017/02/01)</h3>
<ul class=image>
<li class="major bug">
<strong>Important security fixes</strong>
(<a href="https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2017-02-01">security advisory</a>)
</ul>
<h3><a name=v2.43>What's new in 2.43</a> (2017/01/29)</h3>
<ul class=image>
<li class=rfe>
......
......@@ -5,7 +5,7 @@
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version>
<version>2.45-SNAPSHOT</version>
</parent>
<artifactId>cli</artifactId>
......
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version>
<version>2.45-SNAPSHOT</version>
</parent>
<artifactId>jenkins-core</artifactId>
......@@ -159,7 +159,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler-adjunct-timeline</artifactId>
<version>1.4</version>
<version>1.5</version>
</dependency>
<dependency>
<groupId>org.kohsuke.stapler</groupId>
......@@ -591,12 +591,6 @@ THE SOFTWARE.
<version>1.3.1-jenkins-1</version>
</dependency>
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.3m</version>
</dependency>
<!-- offline profiler API to put in the classpath if we need it -->
<!--dependency>
<groupId>com.yourkit.api</groupId>
......
......@@ -51,6 +51,9 @@ import java.util.Collection;
import java.util.List;
import com.jcraft.jzlib.GZIPInputStream;
import com.jcraft.jzlib.GZIPOutputStream;
import hudson.remoting.ClassFilter;
import jenkins.security.HMACConfidentialKey;
import jenkins.util.SystemProperties;
/**
* Data that hangs off from a console output.
......@@ -94,8 +97,6 @@ import com.jcraft.jzlib.GZIPOutputStream;
* {@link ConsoleNote} always sticks to a particular point in the console output.
*
* <p>
* This design allows descendant processes of Hudson to emit {@link ConsoleNote}s. For example, Ant forked
* by a shell forked by Hudson can put an encoded note in its stdout, and Hudson will correctly understands that.
* The preamble and postamble includes a certain ANSI escape sequence designed in such a way to minimize garbage
* if this output is observed by a human being directly.
*
......@@ -121,6 +122,15 @@ import com.jcraft.jzlib.GZIPOutputStream;
* @since 1.349
*/
public abstract class ConsoleNote<T> implements Serializable, Describable<ConsoleNote<?>>, ExtensionPoint {
private static final HMACConfidentialKey MAC = new HMACConfidentialKey(ConsoleNote.class, "MAC");
/**
* Allows historical build records with unsigned console notes to be displayed, at the expense of any security.
* Disables checking of {@link #MAC} so do not set this flag unless you completely trust all users capable of affecting build output,
* which in practice means that all SCM committers as well as all Jenkins users with any non-read-only access are consider administrators.
*/
static /* nonfinal for tests & script console */ boolean INSECURE = SystemProperties.getBoolean(ConsoleNote.class.getName() + ".INSECURE");
/**
* When the line of a console output that this annotation is attached is read by someone,
* a new {@link ConsoleNote} is de-serialized and this method is invoked to annotate that line.
......@@ -179,6 +189,11 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
DataOutputStream dos = new DataOutputStream(new Base64OutputStream(buf2,true,-1,null));
try {
buf2.write(PREAMBLE);
if (Jenkins.getInstanceOrNull() != null) { // else we are in another JVM and cannot sign; result will be ignored unless INSECURE
byte[] mac = MAC.mac(buf.toByteArray());
dos.writeInt(- mac.length); // negative to differentiate from older form
dos.write(mac);
}
dos.writeInt(buf.size());
buf.writeTo(dos);
} finally {
......@@ -211,7 +226,17 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
return null; // not a valid preamble
DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
int sz = decoded.readInt();
int macSz = - decoded.readInt();
byte[] mac;
int sz;
if (macSz > 0) { // new format
mac = new byte[macSz];
decoded.readFully(mac);
sz = decoded.readInt();
} else {
mac = null;
sz = - macSz;
}
byte[] buf = new byte[sz];
decoded.readFully(buf);
......@@ -220,8 +245,18 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
if (!Arrays.equals(postamble,POSTAMBLE))
return null; // not a valid postamble
try (ObjectInputStream ois = new ObjectInputStreamEx(
new GZIPInputStream(new ByteArrayInputStream(buf)), Jenkins.getInstance().pluginManager.uberClassLoader)) {
if (mac == null) {
if (!INSECURE) {
throw new IOException("Refusing to deserialize unsigned note from an old log.");
}
} else if (!MAC.checkMac(buf, mac)) {
throw new IOException("MAC mismatch");
}
Jenkins jenkins = Jenkins.getInstance();
try (ObjectInputStream ois = new ObjectInputStreamEx(new GZIPInputStream(new ByteArrayInputStream(buf)),
jenkins != null ? jenkins.pluginManager.uberClassLoader : ConsoleNote.class.getClassLoader(),
ClassFilter.DEFAULT)) {
return (ConsoleNote) ois.readObject();
}
} catch (Error e) {
......@@ -241,8 +276,15 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
return; // not a valid preamble
DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
int sz = decoded.readInt();
IOUtils.skip(decoded,sz);
int macSz = - decoded.readInt();
if (macSz > 0) { // new format
IOUtils.skip(decoded, macSz);
int sz = decoded.readInt();
IOUtils.skip(decoded, sz);
} else { // old format
int sz = -macSz;
IOUtils.skip(decoded, sz);
}
byte[] postamble = new byte[POSTAMBLE.length];
in.readFully(postamble);
......
......@@ -32,6 +32,7 @@ import org.jenkinsci.Symbol;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.IOException;
import java.util.List;
......@@ -64,6 +65,7 @@ public final class HudsonHomeDiskUsageMonitor extends AdministrativeMonitor {
/**
* Depending on whether the user said "yes" or "no", send him to the right place.
*/
@RequirePOST
public HttpResponse doAct(@QueryParameter String no) throws IOException {
if(no!=null) {
disable(true);
......
......@@ -52,6 +52,7 @@ import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
......
......@@ -37,6 +37,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Looks out for a broken reverse proxy setup that doesn't rewrite the location header correctly.
......@@ -84,6 +85,7 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor {
/**
* Depending on whether the user said "yes" or "no", send him to the right place.
*/
@RequirePOST
public HttpResponse doAct(@QueryParameter String no) throws IOException {
if(no!=null) { // dismiss
disable(true);
......
......@@ -29,6 +29,7 @@ import hudson.Extension;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.IOException;
......@@ -56,6 +57,7 @@ public class TooManyJobsButNoView extends AdministrativeMonitor {
/**
* Depending on whether the user said "yes" or "no", send him to the right place.
*/
@RequirePOST
public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException {
if(req.hasParameter("no")) {
disable(true);
......
......@@ -44,7 +44,6 @@ import hudson.util.Secret;
import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import jenkins.model.Jenkins;
import jenkins.security.NotReallyRoleSensitiveCallable;
import org.acegisecurity.Authentication;
import jenkins.util.xml.XMLUtils;
import org.apache.tools.ant.taskdefs.Copy;
......@@ -75,7 +74,6 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
import org.xml.sax.SAXException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
......@@ -235,27 +233,10 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
if (this.name.equals(newName))
return;
// the test to see if the project already exists or not needs to be done in escalated privilege
// to avoid overwriting
ACL.impersonate(ACL.SYSTEM,new NotReallyRoleSensitiveCallable<Void,IOException>() {
final Authentication user = Jenkins.getAuthentication();
@Override
public Void call() throws IOException {
Item existing = parent.getItem(newName);
if (existing != null && existing!=AbstractItem.this) {
if (existing.getACL().hasPermission(user,Item.DISCOVER))
// the look up is case insensitive, so we need "existing!=this"
// to allow people to rename "Foo" to "foo", for example.
// see http://www.nabble.com/error-on-renaming-project-tt18061629.html
throw new IllegalArgumentException("Job " + newName + " already exists");
else {
// can't think of any real way to hide this, but at least the error message could be vague.
throw new IOException("Unable to rename to " + newName);
}
}
return null;
}
});
// the lookup is case insensitive, so we should not fail if this item was the “existing” one
// to allow people to rename "Foo" to "foo", for example.
// see http://www.nabble.com/error-on-renaming-project-tt18061629.html
Items.verifyItemDoesNotAlreadyExist(parent, newName, this);
File oldRoot = this.getRootDir();
......
......@@ -33,8 +33,12 @@ import java.util.Set;
import java.io.IOException;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Checks the health of a subsystem of Jenkins and if there's something
......@@ -74,7 +78,7 @@ import org.kohsuke.stapler.StaplerResponse;
* @see Jenkins#administrativeMonitors
*/
@LegacyInstancesAreScopedToHudson
public abstract class AdministrativeMonitor extends AbstractModelObject implements ExtensionPoint {
public abstract class AdministrativeMonitor extends AbstractModelObject implements ExtensionPoint, StaplerProxy {
/**
* Human-readable ID of this monitor, which needs to be unique within the system.
*
......@@ -142,12 +146,21 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
/**
* URL binding to disable this monitor.
*/
@RequirePOST
public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
disable(true);
rsp.sendRedirect2(req.getContextPath()+"/manage");
}
/**
* Requires ADMINISTER permission for any operation in here.
*/
@Restricted(NoExternalUse.class)
public Object getTarget() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
return this;
}
/**
* All registered {@link AdministrativeMonitor} instances.
*/
......
......@@ -1110,8 +1110,10 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
@Exported(inline=true)
public Map<String/*monitor name*/,Object> getMonitorData() {
Map<String,Object> r = new HashMap<String, Object>();
for (NodeMonitor monitor : NodeMonitor.getAll())
r.put(monitor.getClass().getName(),monitor.data(this));
if (hasPermission(CONNECT)) {
for (NodeMonitor monitor : NodeMonitor.getAll())
r.put(monitor.getClass().getName(), monitor.data(this));
}
return r;
}
......
......@@ -231,7 +231,7 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
Permission DISCOVER = new Permission(PERMISSIONS, "Discover", Messages._AbstractProject_DiscoverPermission_Description(), READ, PermissionScope.ITEM);
/**
* Ability to view configuration details.
* If the user lacks {@link CONFIGURE} then any {@link Secret}s must be masked out, even in encrypted form.
* If the user lacks {@link #CONFIGURE} then any {@link Secret}s must be masked out, even in encrypted form.
* @see Secret#ENCRYPTED_VALUE_PATTERN
*/
Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._AbstractProject_ExtendedReadPermission_Description(), CONFIGURE, SystemProperties.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.ITEM});
......
......@@ -260,10 +260,7 @@ public abstract class ItemGroupMixIn {
acl.checkPermission(Item.CREATE);
Jenkins.getInstance().getProjectNamingStrategy().checkName(name);
if (parent.getItem(name) != null) {
throw new IllegalArgumentException(parent.getDisplayName() + " already contains an item '" + name + "'");
}
// TODO what if we have no DISCOVER permission on the existing job?
Items.verifyItemDoesNotAlreadyExist(parent, name, null);
// place it as config.xml
File configXml = Items.getConfigFile(getRootDirFor(name)).getFile();
......@@ -316,9 +313,7 @@ public abstract class ItemGroupMixIn {
acl.getACL().checkCreatePermission(parent, type);
Jenkins.getInstance().getProjectNamingStrategy().checkName(name);
if(parent.getItem(name)!=null)
throw new IllegalArgumentException("Project of the name "+name+" already exists");
// TODO problem with DISCOVER as noted above
Items.verifyItemDoesNotAlreadyExist(parent, name, null);
TopLevelItem item = type.newInstance(parent, name);
try {
......
......@@ -493,9 +493,7 @@ public class Items {
throw new IllegalArgumentException();
}
String name = item.getName();
if (destination.getItem(name) != null) {
throw new IllegalArgumentException(name + " already exists");
}
verifyItemDoesNotAlreadyExist(destination, name, null);
String oldFullName = item.getFullName();
// TODO AbstractItem.renameTo has a more baroque implementation; factor it out into a utility method perhaps?
File destDir = destination.getRootDirFor(item);
......@@ -623,6 +621,30 @@ public class Items {
}
}
/**
* Securely check for the existence of an item before trying to create one with the same name.
* @param parent the folder where we are about to create/rename/move an item
* @param newName the proposed new name
* @param variant if not null, an existing item which we accept could be there
* @throws IllegalArgumentException if there is already something there, which you were supposed to know about
* @throws Failure if there is already something there but you should not be told details
*/
static void verifyItemDoesNotAlreadyExist(@Nonnull ItemGroup<?> parent, @Nonnull String newName, @CheckForNull Item variant) throws IllegalArgumentException, Failure {
Item existing;
try (ACLContext ctxt = ACL.as(ACL.SYSTEM)) {
existing = parent.getItem(newName);
}
if (existing != null && existing != variant) {
if (existing.hasPermission(Item.DISCOVER)) {
String prefix = parent.getFullName();
throw new IllegalArgumentException((prefix.isEmpty() ? "" : prefix + "/") + newName + " already exists");
} else {
// Cannot hide its existence, so at least be as vague as possible.
throw new Failure("");
}
}
}
/**
* Used to load/save job configuration.
*
......
......@@ -31,11 +31,16 @@ import hudson.scm.SCM;
import hudson.tasks.BuildWrapper;
import hudson.tasks.Builder;
import hudson.util.VariableResolver;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
......@@ -70,6 +75,9 @@ import org.kohsuke.stapler.export.ExportedBean;
*/
@ExportedBean(defaultVisibility=3)
public abstract class ParameterValue implements Serializable {
private static final Logger LOGGER = Logger.getLogger(ParameterValue.class.getName());
protected final String name;
private String description;
......@@ -91,6 +99,16 @@ public abstract class ParameterValue implements Serializable {
this.description = description;
}
@Restricted(DoNotUse.class) // for value.jelly
public String getFormattedDescription() {
try {
return Jenkins.getInstance().getMarkupFormatter().translate(description);
} catch (IOException e) {
LOGGER.warning("failed to translate description using configured markup formatter");
return "";
}
}
/**
* Name of the parameter.
*
......
......@@ -1122,5 +1122,20 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* JENKINS-22346.
*/
public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName()+".allowNonExistentUserToLogin");
}
/**
* Jenkins historically created a (usually) ephemeral user record when an user with Overall/Administer permission
* accesses a /user/arbitraryName URL.
* <p>
* Unfortunately this constitutes a CSRF vulnerability, as malicious users can make admins create arbitrary numbers
* of ephemeral user records, so the behavior was changed in Jenkins 2.TODO / 2.32.2.
* <p>
* As some users may be relying on the previous behavior, setting this to true restores the previous behavior. This
* is not recommended.
*
* SECURITY-406.
*/
@Restricted(NoExternalUse.class)
public static boolean ALLOW_USER_CREATION_VIA_URL = SystemProperties.getBoolean(User.class.getName() + ".allowUserCreationViaUrl");
}
此差异已折叠。
......@@ -60,7 +60,6 @@ import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.dao.DataAccessException;
import javax.servlet.Filter;
......
......@@ -24,16 +24,19 @@
package hudson.slaves;
import jenkins.model.Jenkins;
import hudson.Functions;
import hudson.model.Computer;
import hudson.model.User;
import jenkins.model.Jenkins;
import org.jvnet.localizer.Localizable;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.export.Exported;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.ObjectStreamException;
import java.util.Collections;
import java.util.Date;
/**
......@@ -128,21 +131,49 @@ public abstract class OfflineCause {
/**
* Taken offline by user.
*
* @since 1.551
*/
public static class UserCause extends SimpleOfflineCause {
private final User user;
public UserCause(User user, String message) {
super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(
user!=null ? user.getId() : Jenkins.ANONYMOUS.getName(),
@Deprecated
private transient User user;
// null when unknown
private /*final*/ @CheckForNull String userId;
public UserCause(@CheckForNull User user, @CheckForNull String message) {
this(
user != null ? user.getId() : null,
message != null ? " : " + message : ""
));
this.user = user;
);
}
private UserCause(String userId, String message) {
super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(userId != null ? userId : Jenkins.ANONYMOUS.getName(), message));
this.userId = userId;
}
public User getUser() {
return user;
return userId == null
? User.getUnknown()
: User.getById(userId, true)
;
}
// Storing the User in a filed was a mistake, switch to userId
@SuppressWarnings("deprecation")
private Object readResolve() throws ObjectStreamException {
if (user != null) {
String id = user.getId();
if (id != null) {
userId = id;
} else {
// The user field is not properly deserialized so id may be missing. Look the user up by fullname
User user = User.get(this.user.getFullName(), true, Collections.emptyMap());
userId = user.getId();
}
this.user = null;
}
return this;
}
}
......
......@@ -80,6 +80,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static hudson.tools.JDKInstaller.Preference.*;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Install JDKs from java.sun.com.
......@@ -788,7 +789,9 @@ public class JDKInstaller extends ToolInstaller {
/**
* Submits the Oracle account username/password.
*/
@RequirePOST
public HttpResponse doPostCredential(@QueryParameter String username, @QueryParameter String password) throws IOException, ServletException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
this.username = username;
this.password = Secret.fromString(password);
save();
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2016, 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.util;
import com.trilead.ssh2.crypto.Base64;
import hudson.Util;
import jenkins.model.Jenkins;
import jenkins.security.CryptoConfidentialKey;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.security.GeneralSecurityException;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Historical algorithms for decrypting {@link Secret}s.
*/
@Restricted(NoExternalUse.class)
public class HistoricalSecrets {
/*package*/ static Secret decrypt(String data, CryptoConfidentialKey key) throws IOException, GeneralSecurityException {
byte[] in = Base64.decode(data.toCharArray());
Secret s = tryDecrypt(key.decrypt(), in);
if (s!=null) return s;
// try our historical key for backward compatibility
Cipher cipher = Secret.getCipher("AES");
cipher.init(Cipher.DECRYPT_MODE, getLegacyKey());
return tryDecrypt(cipher, in);
}
/*package*/ static Secret tryDecrypt(Cipher cipher, byte[] in) {
try {
String plainText = new String(cipher.doFinal(in), UTF_8);
if(plainText.endsWith(MAGIC))
return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
return null;
} catch (GeneralSecurityException e) {
return null; // if the key doesn't match with the bytes, it can result in BadPaddingException
}
}
/**
* Turns {@link Jenkins#getSecretKey()} into an AES key.
*
* @deprecated
* This is no longer the key we use to encrypt new information, but we still need this
* to be able to decrypt what's already persisted.
*/
@Deprecated
/*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException {
String secret = Secret.SECRET;
if(secret==null) return Jenkins.getInstance().getSecretKeyAsAES128();
return Util.toAes128Key(secret);
}
private static final String MAGIC = "::::MAGIC::::";
}
......@@ -2,6 +2,7 @@
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2016, 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
......@@ -30,12 +31,12 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.trilead.ssh2.crypto.Base64;
import jenkins.util.SystemProperties;
import java.util.Arrays;
import jenkins.model.Jenkins;
import hudson.Util;
import jenkins.security.CryptoConfidentialKey;
import org.kohsuke.stapler.Stapler;
import javax.crypto.SecretKey;
import javax.crypto.Cipher;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
......@@ -47,6 +48,8 @@ import javax.annotation.Nonnull;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Glorified {@link String} that uses encryption in the persisted form, to avoid accidental exposure of a secret.
*
......@@ -61,14 +64,21 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
* @author Kohsuke Kawaguchi
*/
public final class Secret implements Serializable {
private static final byte PAYLOAD_V1 = 1;
/**
* Unencrypted secret text.
*/
@Nonnull
private final String value;
private byte[] iv;
/*package*/ Secret(String value) {
this.value = value;
}
private Secret(String value) {
/*package*/ Secret(String value, byte[] iv) {
this.value = value;
this.iv = iv;
}
/**
......@@ -105,20 +115,6 @@ public final class Secret implements Serializable {
return value.hashCode();
}
/**
* Turns {@link Jenkins#getSecretKey()} into an AES key.
*
* @deprecated
* This is no longer the key we use to encrypt new information, but we still need this
* to be able to decrypt what's already persisted.
*/
@Deprecated
/*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException {
String secret = SECRET;
if(secret==null) return Jenkins.getInstance().getSecretKeyAsAES128();
return Util.toAes128Key(secret);
}
/**
* Encrypts {@link #value} and returns it in an encoded printable form.
*
......@@ -126,23 +122,42 @@ public final class Secret implements Serializable {
*/
public String getEncryptedValue() {
try {
Cipher cipher = KEY.encrypt();
// add the magic suffix which works like a check sum.
return new String(Base64.encode(cipher.doFinal((value+MAGIC).getBytes("UTF-8"))));
synchronized (this) {
if (iv == null) { //if we were created from plain text or other reason without iv
iv = KEY.newIv();
}
}
Cipher cipher = KEY.encrypt(iv);
byte[] encrypted = cipher.doFinal(this.value.getBytes(UTF_8));
byte[] payload = new byte[1 + 8 + iv.length + encrypted.length];
int pos = 0;
// For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
payload[pos++] = PAYLOAD_V1;
payload[pos++] = (byte)(iv.length >> 24);
payload[pos++] = (byte)(iv.length >> 16);
payload[pos++] = (byte)(iv.length >> 8);
payload[pos++] = (byte)(iv.length);
payload[pos++] = (byte)(encrypted.length >> 24);
payload[pos++] = (byte)(encrypted.length >> 16);
payload[pos++] = (byte)(encrypted.length >> 8);
payload[pos++] = (byte)(encrypted.length);
System.arraycopy(iv, 0, payload, pos, iv.length);
pos+=iv.length;
System.arraycopy(encrypted, 0, payload, pos, encrypted.length);
return "{"+new String(Base64.encode(payload))+"}";
} catch (GeneralSecurityException e) {
throw new Error(e); // impossible
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
}
}
/**
* Pattern matching a possible output of {@link #getEncryptedValue}.
* Basically, any Base64-encoded value.
* You must then call {@link #decrypt} to eliminate false positives.
* Pattern matching a possible output of {@link #getEncryptedValue}
* Basically, any Base64-encoded value optionally wrapped by {@code {}}.
* You must then call {@link #decrypt(String)} to eliminate false positives.
* @see #ENCRYPTED_VALUE_PATTERN
*/
@Restricted(NoExternalUse.class)
public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("[A-Za-z0-9+/]+={0,2}");
public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?");
/**
* Reverse operation of {@link #getEncryptedValue()}. Returns null
......@@ -151,32 +166,52 @@ public final class Secret implements Serializable {
@CheckForNull
public static Secret decrypt(@CheckForNull String data) {
if(data==null) return null;
try {
byte[] in = Base64.decode(data.toCharArray());
Secret s = tryDecrypt(KEY.decrypt(), in);
if (s!=null) return s;
// try our historical key for backward compatibility
Cipher cipher = getCipher("AES");
cipher.init(Cipher.DECRYPT_MODE, getLegacyKey());
return tryDecrypt(cipher, in);
} catch (GeneralSecurityException e) {
return null;
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
} catch (IOException e) {
return null;
}
}
/*package*/ static Secret tryDecrypt(Cipher cipher, byte[] in) throws UnsupportedEncodingException {
try {
String plainText = new String(cipher.doFinal(in), "UTF-8");
if(plainText.endsWith(MAGIC))
return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
return null;
} catch (GeneralSecurityException e) {
return null; // if the key doesn't match with the bytes, it can result in BadPaddingException
if (data.startsWith("{") && data.endsWith("}")) { //likely CBC encrypted/containing metadata but could be plain text
byte[] payload;
try {
payload = Base64.decode(data.substring(1, data.length()-1).toCharArray());
} catch (IOException e) {
return null;
}
switch (payload[0]) {
case PAYLOAD_V1:
// For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
int ivLength = ((payload[1] & 0xff) << 24)
| ((payload[2] & 0xff) << 16)
| ((payload[3] & 0xff) << 8)
| (payload[4] & 0xff);
int dataLength = ((payload[5] & 0xff) << 24)
| ((payload[6] & 0xff) << 16)
| ((payload[7] & 0xff) << 8)
| (payload[8] & 0xff);
if (payload.length != 1 + 8 + ivLength + dataLength) {
// not valid v1
return null;
}
byte[] iv = Arrays.copyOfRange(payload, 9, 9 + ivLength);
byte[] code = Arrays.copyOfRange(payload, 9+ivLength, payload.length);
String text;
try {
text = new String(KEY.decrypt(iv).doFinal(code), UTF_8);
} catch (GeneralSecurityException e) {
// it's v1 which cannot be historical, but not decrypting
return null;
}
return new Secret(text, iv);
default:
return null;
}
} else {
try {
return HistoricalSecrets.decrypt(data, KEY);
} catch (GeneralSecurityException e) {
return null;
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
} catch (IOException e) {
return null;
}
}
}
......@@ -234,8 +269,6 @@ public final class Secret implements Serializable {
}
}
private static final String MAGIC = "::::MAGIC::::";
/**
* Workaround for JENKINS-6459 / http://java.net/jira/browse/GLASSFISH-11862
* @see #getCipher(String)
......@@ -252,6 +285,14 @@ public final class Secret implements Serializable {
*/
private static final CryptoConfidentialKey KEY = new CryptoConfidentialKey(Secret.class.getName());
/**
* Reset the internal secret key for testing.
*/
@Restricted(NoExternalUse.class)
/*package*/ static void resetKeyForTest() {
KEY.resetForTest();
}
private static final long serialVersionUID = 1L;
static {
......
......@@ -3,7 +3,6 @@ package hudson.util;
import com.trilead.ssh2.crypto.Base64;
import hudson.Functions;
import hudson.model.TaskListener;
import org.apache.commons.io.FileUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
......@@ -35,21 +34,21 @@ public class SecretRewriter {
*/
private int count;
/**
* If non-null the original file before rewrite gets in here.
*/
private final File backupDirectory;
/**
* Canonical paths of the directories we are recursing to protect
* against symlink induced cycles.
*/
private Set<String> callstack = new HashSet<String>();
public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
public SecretRewriter() throws GeneralSecurityException {
cipher = Secret.getCipher("AES");
key = Secret.getLegacyKey();
this.backupDirectory = backupDirectory;
key = HistoricalSecrets.getLegacyKey();
}
/** @deprecated SECURITY-376: {@code backupDirectory} is ignored */
@Deprecated
public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
this();
}
private String tryRewrite(String s) throws IOException, InvalidKeyException {
......@@ -65,19 +64,21 @@ public class SecretRewriter {
return s; // not a valid base64
}
cipher.init(Cipher.DECRYPT_MODE, key);
Secret sec = Secret.tryDecrypt(cipher, in);
Secret sec = HistoricalSecrets.tryDecrypt(cipher, in);
if(sec!=null) // matched
return sec.getEncryptedValue(); // replace by the new encrypted value
else // not encrypted with the legacy key. leave it unmodified
return s;
}
/**
* @param backup
* if non-null, the original file will be copied here before rewriting.
* if the rewrite doesn't happen, no copying.
*/
/** @deprecated SECURITY-376: {@code backup} is ignored */
@Deprecated
public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException {
return rewrite(f);
}
public boolean rewrite(File f) throws InvalidKeyException, IOException {
AtomicFileWriter w = new AtomicFileWriter(f, "UTF-8");
try {
......@@ -113,10 +114,6 @@ public class SecretRewriter {
}
if (modified) {
if (backup!=null) {
backup.getParentFile().mkdirs();
FileUtils.copyFile(f,backup);
}
w.commit();
}
return modified;
......@@ -161,11 +158,7 @@ public class SecretRewriter {
if ((count++)%100==0)
listener.getLogger().println("Scanning "+child);
try {
File backup = null;
if (backupDirectory!=null) backup = new File(backupDirectory,relative+'/'+ cn);
if (rewrite(child,backup)) {
if (backup!=null)
listener.getLogger().println("Copied "+child+" to "+backup+" as a backup");
if (rewrite(child)) {
listener.getLogger().println("Rewritten "+child);
rewritten++;
}
......@@ -195,7 +188,6 @@ public class SecretRewriter {
String n = dir.getName();
return n.equals("workspace") || n.equals("artifacts")
|| n.equals("plugins") // no mutable data here
|| n.equals("jenkins.security.RekeySecretAdminMonitor") // we don't want to rewrite backups
|| n.equals(".") || n.equals("..");
}
......
......@@ -6,6 +6,7 @@ import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.IOException;
......@@ -34,6 +35,7 @@ public class SecurityIsOffMonitor extends AdministrativeMonitor {
/**
* Depending on whether the user said "yes" or "no", send him to the right place.
*/
@RequirePOST
public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException {
if(req.hasParameter("no")) {
disable(true);
......
......@@ -118,7 +118,6 @@ import hudson.security.AccessControlled;
import hudson.security.AuthorizationStrategy;
import hudson.security.BasicAuthenticationFilter;
import hudson.security.FederatedLoginService;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.security.HudsonFilter;
import hudson.security.LegacyAuthorizationStrategy;
import hudson.security.LegacySecurityRealm;
......@@ -1694,11 +1693,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
*/
@Exported(name="jobs")
public List<TopLevelItem> getItems() {
if (authorizationStrategy instanceof AuthorizationStrategy.Unsecured ||
authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) {
return new ArrayList(items.values());
}
List<TopLevelItem> viewableItems = new ArrayList<TopLevelItem>();
for (TopLevelItem item : items.values()) {
if (item.hasPermission(Item.READ))
......@@ -2272,7 +2266,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
})
.add(new CollectionSearchIndex() {// for views
protected View get(String key) { return getView(key); }
protected Collection<View> all() { return views; }
protected Collection<View> all() { return viewGroupMixIn.getViews(); }
});
return builder;
}
......@@ -2871,11 +2865,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
/**
* Gets the user of the given name.
*
* @return the user of the given name (which may or may not be an id), if that person exists or the invoker {@link #hasPermission} on {@link #ADMINISTER}; else null
* @return the user of the given name (which may or may not be an id), if that person exists; else null
* @see User#get(String,boolean), {@link User#getById(String, boolean)}
*/
public @CheckForNull User getUser(String name) {
return User.get(name,hasPermission(ADMINISTER));
return User.get(name, User.ALLOW_USER_CREATION_VIA_URL && hasPermission(ADMINISTER));
}
public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name ) throws IOException {
......@@ -4354,6 +4348,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
@RequirePOST
public void doFingerprintCleanup(StaplerResponse rsp) throws IOException {
checkPermission(ADMINISTER);
FingerprintCleanupThread.invoke();
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
......@@ -4362,6 +4357,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
@RequirePOST
public void doWorkspaceCleanup(StaplerResponse rsp) throws IOException {
checkPermission(ADMINISTER);
WorkspaceCleanupThread.invoke();
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
......
package jenkins.security;
import hudson.Main;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
......@@ -15,6 +21,9 @@ import java.security.GeneralSecurityException;
* @since 1.498
*/
public class CryptoConfidentialKey extends ConfidentialKey {
@Restricted(NoExternalUse.class) // TODO pending API
public static final int DEFAULT_IV_LENGTH = 16;
private volatile SecretKey secret;
public CryptoConfidentialKey(String id) {
super(id);
......@@ -35,7 +44,7 @@ public class CryptoConfidentialKey extends ConfidentialKey {
store(payload);
}
// Due to the stupid US export restriction JDK only ships 128bit version.
secret = new SecretKeySpec(payload,0,128/8, ALGORITHM);
secret = new SecretKeySpec(payload,0,128/8, KEY_ALGORITHM);
}
}
}
......@@ -47,10 +56,12 @@ public class CryptoConfidentialKey extends ConfidentialKey {
/**
* Returns a {@link Cipher} object for encrypting with this key.
* @deprecated use {@link #encrypt(byte[])}
*/
@Deprecated
public Cipher encrypt() {
try {
Cipher cipher = Secret.getCipher(ALGORITHM);
Cipher cipher = Secret.getCipher(KEY_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, getKey());
return cipher;
} catch (GeneralSecurityException e) {
......@@ -58,12 +69,68 @@ public class CryptoConfidentialKey extends ConfidentialKey {
}
}
/**
* Returns a {@link Cipher} object for encrypting with this key using the provided initialization vector.
* @param iv the initialization vector
* @return the cipher
*/
@Restricted(NoExternalUse.class) // TODO pending API
public Cipher encrypt(byte[] iv) {
try {
Cipher cipher = Secret.getCipher(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, getKey(), new IvParameterSpec(iv));
return cipher;
} catch (GeneralSecurityException e) {
throw new AssertionError(e);
}
}
/**
* Returns a {@link Cipher} object for decrypting with this key using the provided initialization vector.
* @param iv the initialization vector
* @return the cipher
*/
@Restricted(NoExternalUse.class) // TODO pending ApI
public Cipher decrypt(byte[] iv) {
try {
Cipher cipher = Secret.getCipher(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getKey(), new IvParameterSpec(iv));
return cipher;
} catch (GeneralSecurityException e) {
throw new AssertionError(e);
}
}
/**
* Generates a new Initialization Vector.
* @param length the length of the salt
* @return some random bytes
* @see #encrypt(byte[])
*/
@Restricted(NoExternalUse.class) // TODO pending API
public byte[] newIv(int length) {
return ConfidentialStore.get().randomBytes(length);
}
/**
* Generates a new Initialization Vector of default length.
* @return some random bytes
* @see #newIv(int)
* @see #encrypt(byte[])
*/
@Restricted(NoExternalUse.class) // TODO pending API
public byte[] newIv() {
return newIv(DEFAULT_IV_LENGTH);
}
/**
* Returns a {@link Cipher} object for decrypting with this key.
* @deprecated use {@link #decrypt(byte[])}
*/
@Deprecated
public Cipher decrypt() {
try {
Cipher cipher = Secret.getCipher(ALGORITHM);
Cipher cipher = Secret.getCipher(KEY_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getKey());
return cipher;
} catch (GeneralSecurityException e) {
......@@ -72,5 +139,18 @@ public class CryptoConfidentialKey extends ConfidentialKey {
}
private static final String ALGORITHM = "AES";
private static final String KEY_ALGORITHM = "AES";
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
/**
* Reset the internal secret key for testing.
*/
@Restricted(NoExternalUse.class)
public void resetForTest() {
if (Main.isUnitTest) {
this.secret = null;
} else {
throw new IllegalStateException("Only for testing");
}
}
}
package jenkins.security;
import hudson.Extension;
import hudson.Util;
import hudson.Functions;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
......@@ -13,7 +14,6 @@ import jenkins.model.Jenkins;
import jenkins.util.io.FileBoolean;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST;
......@@ -31,7 +31,7 @@ import java.util.logging.Logger;
* @author Kohsuke Kawaguchi
*/
@Extension @Symbol("rekeySecret")
public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor implements StaplerProxy {
public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor {
/**
* Whether we detected a need to run the rewrite program.
......@@ -53,6 +53,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i
*/
private final FileBoolean scanOnBoot = state("scanOnBoot");
@SuppressWarnings("OverridableMethodCallInConstructor") // should have been final
public RekeySecretAdminMonitor() throws IOException {
// if JENKINS_HOME existed <1.497, we need to offer rewrite
// this computation needs to be done and the value be captured,
......@@ -62,14 +63,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i
if (j.isUpgradedFromBefore(new VersionNumber("1.496.*"))
&& new FileBoolean(new File(j.getRootDir(),"secret.key.not-so-secret")).isOff())
needed.on();
}
/**
* Requires ADMINISTER permission for any operation in here.
*/
public Object getTarget() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
return this;
Util.deleteRecursive(new File(getBaseDir(), "backups")); // SECURITY-376: no longer used
}
@Override
......@@ -142,7 +136,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor i
protected void fix(TaskListener listener) throws Exception {
LOGGER.info("Initiating a re-keying of secrets. See "+getLogFile());
SecretRewriter rewriter = new SecretRewriter(new File(getBaseDir(),"backups"));
SecretRewriter rewriter = new SecretRewriter();
try {
PrintStream log = listener.getLogger();
......
......@@ -9,6 +9,7 @@ import org.jenkinsci.Symbol;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.inject.Inject;
import java.io.IOException;
......@@ -50,6 +51,7 @@ public class AdminCallableMonitor extends AdministrativeMonitor {
/**
* Depending on whether the user said "examin" or "dismiss", send him to the right place.
*/
@RequirePOST
public HttpResponse doAct(@QueryParameter String dismiss) throws IOException {
if(dismiss!=null) {
disable(true);
......
......@@ -5,6 +5,7 @@ import hudson.model.AdministrativeMonitor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.inject.Inject;
import java.io.IOException;
......@@ -33,6 +34,7 @@ public class MasterKillSwitchWarning extends AdministrativeMonitor {
return Messages.MasterKillSwitchWarning_DisplayName();
}
@RequirePOST
public HttpResponse doAct(@QueryParameter String dismiss) throws IOException {
if(dismiss!=null) {
disable(true);
......
......@@ -24,7 +24,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">
<l:layout title="${%JENKINS_HOME is almost full}" permission="${app.ADMINISTER}">
<l:layout title="${%JENKINS_HOME is almost full}">
<l:main-panel>
<h1>
<l:icon class="icon-warning icon-xlg"/>
......
......@@ -24,7 +24,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">
<l:layout title="${%Manage Old Data}" permission="${app.ADMINISTER}">
<l:layout title="${%Manage Old Data}">
<st:include page="sidepanel.jelly" it="${app}"/>
<l:main-panel>
<h1>${%Manage Old Data}</h1>
......
# This file is under the MIT License by authors
Build=fghg
Build\ Artifacts=gfhfgh
Took=fghg
startedAgo=dhgg
Build=Eraiki
Build\ Artifacts=Laguntzaileak eraiki
Took=Hartu
startedAgo=orain dela zenbat hasia
# This file is under the MIT License by authors
Back\ to\ Project=drthdf
Changes=hgdg
Console\ Output=ghdgfh
Edit\ Build\ Information=Kompilazioaren argibidea edidatu
Status=hgfdhg
View\ Build\ Information=dhgg
Back\ to\ Project=Proiektura itzuli
Changes=Aldaketak
Console\ Output=Kontsolaren irteera
Edit\ Build\ Information=Konpilazioaren argibideak edidatu
Status=Egoera
View\ Build\ Information=Konpilazioaren egoera ikusi
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}" />
<f:checkbox name="value" checked="${it.defaultValue}" />
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:checkbox name="value" checked="${it.value}" readonly="true" />
</f:entry>
</j:jelly>
\ No newline at end of file
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}" />
<select name="value">
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}" />
<input name="file" type="file" jsonAware="true" />
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<j:if test="${it.originalFileName != null}">
<j:invokeStatic var="encodedName" className="hudson.Util" method="rawEncode">
<j:arg value="${it.name}" />
......
......@@ -40,6 +40,7 @@ THE SOFTWARE.
<div class="col-xs-24 pane-header">${%Parameters}</div>
<div class="row col-xs-24 pane-content">
<table class="pane">
<j:set var="escapeEntryTitleAndDescription" value="true"/> <!-- SECURITY-353 defense unless overridden -->
<j:forEach var="parameterValue" items="${it.parameters}">
<tbody>
<st:include it="${parameterValue}"
......
......@@ -45,6 +45,7 @@ THE SOFTWARE.
<f:form method="post" action="build${empty(delay)?'':'?delay='+delay}" name="parameters"
tableClass="parameters">
<j:forEach var="parameterDefinition" items="${it.parameterDefinitions}">
<j:set var="escapeEntryTitleAndDescription" value="true"/> <!-- SECURITY-353 defense unless overridden -->
<tbody>
<st:include it="${parameterDefinition}"
page="${parameterDefinition.descriptor.valuePage}" />
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}" />
<f:password name="value" value="${it.DEFAULT_VALUE}"/>
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:password name="value" value="********" readonly="true" />
</f:entry>
</j:jelly>
\ No newline at end of file
# This file is under the MIT License by authors
Delete\ this\ build=Kompilazioa ezabatu
Delete\ this\ build=Konpilazioa ezabatu
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}" />
<select name="runId">
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<a href="${rootURL}/${it.run.url}" class="model-link inside">${it.run.fullDisplayName}</a>
</f:entry>
</j:jelly>
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}" />
<f:textbox name="value" value="${it.defaultValue}" />
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:textbox name="value" value="${it.value}" readonly="true" />
</f:entry>
</j:jelly>
\ No newline at end of file
......@@ -24,7 +24,8 @@ 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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.formattedDescription}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}"/>
<f:textarea name="value" value="${it.defaultValue}"/>
......
......@@ -26,7 +26,8 @@ THE SOFTWARE.
<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" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}">
<j:set var="escapeEntryTitleAndDescription" value="false"/>
<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:textarea name="value" value="${it.value}" readonly="readonly" />
</f:entry>
</j:jelly>
\ No newline at end of file
......@@ -4,4 +4,4 @@ Builds=Kompilazioak
Configure=Itxuratu
My\ Views=Nire Begiak
People=Jendea
Status=Estatus
Status=Egoera
# This file is under the MIT License by authors
People=Gendea
People=Jendea
......@@ -23,6 +23,9 @@ allow read,stat <JENKINS_HOME>/userContent($|/.*)
# In the next rule we grant general access under build directories, so first we protect
# the actual build record that Jenkins core reads, which nothing should be touching.
deny all <BUILDDIR>/build.xml
# Similarly for Pipeline build (WorkflowRun) metadata:
deny all <BUILDDIR>/program.dat
deny all <BUILDDIR>/workflow($|/.*)
# Various plugins read/write files under build directories, so allow them all.
# - git 1.x writes changelog.xml from the slave (2.x writes from the master so need not be listed)
......
......@@ -32,6 +32,8 @@ THE SOFTWARE.
<st:attribute name="title">
Name of the entry. Think of this like a label for the control.
This content is HTML (unless the boolean variable escapeEntryTitleAndDescription is set). Use h.escape if necessary.
</st:attribute>
<st:attribute name="field">
Used for the databinding. TBD. When this attribute
......@@ -46,6 +48,8 @@ THE SOFTWARE.
This text shouldn't get too long, and in recent Hudson, this feature
is somewhat de-emphasized, in favor of the inline foldable help page
specified via @help.
This content is HTML (unless the boolean variable escapeEntryTitleAndDescription is set). Use h.escape if necessary.
</st:attribute>
<st:attribute name="help">
URL to the HTML page. When this attribute is specified, the entry gets
......@@ -67,7 +71,7 @@ THE SOFTWARE.
<tr>
<td class="setting-leftspace"><st:nbsp/></td>
<td class="setting-name">
<j:out value="${attrs.title}" />
<j:out value="${escapeEntryTitleAndDescription ? h.escape(attrs.title) : attrs.title}" />
</td>
<td class="setting-main">
<d:invokeBody />
......@@ -78,7 +82,7 @@ THE SOFTWARE.
<tr class="validation-error-area"><td colspan="2" /><td /><td /></tr>
<j:if test="${!empty(attrs.description)}">
<f:description>
<j:out value="${description}"/>
<j:out value="${escapeEntryTitleAndDescription ? h.escape(attrs.description) : attrs.description}"/>
</f:description>
</j:if>
<j:if test="${attrs.help!=null}">
......
......@@ -23,42 +23,49 @@ class SecretRewriterTest {
@Rule public TemporaryFolder tmp = new TemporaryFolder()
def FOO_PATTERN = /<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>/
def MSG_PATTERN = /<msg>\{[A-Za-z0-9+\/]+={0,2}}<\/msg>/
def FOO_PATTERN2 = /(<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>){2}/
def ABC_FOO_PATTERN = /<abc>\s<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>\s<\/abc>/
@Test
void singleFileRewrite() {
def o = encryptOld('foobar') // old
def n = encryptNew('foobar') // new
roundtrip "<foo>${o}</foo>",
"<foo>${n}</foo>"
{assert it ==~ FOO_PATTERN}
roundtrip "<foo>${o}</foo><foo>${o}</foo>",
"<foo>${n}</foo><foo>${n}</foo>"
{assert it ==~ FOO_PATTERN2}
roundtrip "<foo>${n}</foo>",
"<foo>${n}</foo>"
{assert it == "<foo>${n}</foo>"}
roundtrip " <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo> ",
" <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo> "
{assert it == "<foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo>"}
// to be rewritten, it needs to be between a tag
roundtrip "<foo>$o", "<foo>$o"
roundtrip "$o</foo>", "$o</foo>"
roundtrip "<foo>$o", {assert it == "<foo>$o"}
roundtrip "$o</foo>", {assert it == "$o</foo>"}
//
roundtrip "<abc>\n<foo>$o</foo>\n</abc>", "<abc>\n<foo>$n</foo>\n</abc>"
roundtrip "<abc>\n<foo>$o</foo>\n</abc>", {assert it ==~ ABC_FOO_PATTERN}
}
void roundtrip(String before, String after) {
void roundtrip(String before, Closure check) {
def sr = new SecretRewriter(null);
def f = File.createTempFile("test", "xml", tmp.root)
f.text = before
sr.rewrite(f,null)
assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim()
check(f.text.replaceAll(System.getProperty("line.separator"), "\n").trim())
//assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim()
}
String encryptOld(str) {
def cipher = Secret.getCipher("AES");
cipher.init(Cipher.ENCRYPT_MODE, Secret.legacyKey);
return new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8"))))
cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.legacyKey);
return new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8"))))
}
String encryptNew(str) {
......@@ -70,8 +77,7 @@ class SecretRewriterTest {
*/
@Test
void recursionDetection() {
def backup = tmp.newFolder("backup")
def sw = new SecretRewriter(backup);
def sw = new SecretRewriter();
def st = StreamTaskListener.fromStdout()
def o = encryptOld("Hello world")
......@@ -100,12 +106,11 @@ class SecretRewriterTest {
assert 6==sw.rewriteRecursive(t, st)
dirs.each { p->
assert new File(t,"$p/foo.xml").text.trim()==answer
assert new File(backup,"$p/foo.xml").text.trim()==payload
assert new File(t,"$p/foo.xml").text.trim() ==~ MSG_PATTERN
}
// t2 is only reachable by following a symlink. this should be covered, too
assert new File(t2,"foo.xml").text.trim()==answer.trim();
assert new File(t2,"foo.xml").text.trim() ==~ MSG_PATTERN
}
}
......@@ -31,7 +31,8 @@ import org.junit.Rule
import org.junit.Test
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.Cipher
import java.util.regex.Pattern;
/**
* @author Kohsuke Kawaguchi
......@@ -43,6 +44,8 @@ public class SecretTest {
@Rule
public MockSecretRule mockSecretRule = new MockSecretRule()
static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?");
@Test
void testEncrypt() {
def secret = Secret.fromString("abc");
......@@ -54,6 +57,11 @@ public class SecretTest {
// can we round trip?
assert secret==Secret.fromString(secret.encryptedValue);
//Two consecutive encryption requests of the same object should result in the same encrypted value - SECURITY-304
assert secret.encryptedValue == secret.encryptedValue
//Two consecutive encryption requests of different objects with the same value should not result in the same encrypted value - SECURITY-304
assert secret.encryptedValue != Secret.fromString(secret.plainText).encryptedValue
}
@Test
......@@ -62,9 +70,16 @@ public class SecretTest {
String plaintext = RandomStringUtils.random(new Random().nextInt(i));
String ciphertext = Secret.fromString(plaintext).getEncryptedValue();
//println "${plaintext} → ${ciphertext}"
assert Secret.ENCRYPTED_VALUE_PATTERN.matcher(ciphertext).matches();
assert ENCRYPTED_VALUE_PATTERN.matcher(ciphertext).matches();
}
assert !Secret.ENCRYPTED_VALUE_PATTERN.matcher("hello world").matches();
//Not "plain" text
assert !ENCRYPTED_VALUE_PATTERN.matcher("hello world").matches();
//Not "plain" text
assert !ENCRYPTED_VALUE_PATTERN.matcher("helloworld!").matches();
//legacy key
assert ENCRYPTED_VALUE_PATTERN.matcher("abcdefghijklmnopqr0123456789").matches();
//legacy key
assert ENCRYPTED_VALUE_PATTERN.matcher("abcdefghijklmnopqr012345678==").matches();
}
@Test
......@@ -77,7 +92,7 @@ public class SecretTest {
def s = Secret.fromString("Mr.Jenkins");
def xml = Jenkins.XSTREAM.toXML(s);
assert !xml.contains(s.plainText)
assert xml.contains(s.encryptedValue)
assert xml ==~ /<hudson\.util\.Secret>\{[A-Za-z0-9+\/]+={0,2}}<\/hudson\.util\.Secret>/
def o = Jenkins.XSTREAM.fromXML(xml);
assert o==s : xml;
......@@ -104,11 +119,11 @@ public class SecretTest {
*/
@Test
void migrationFromLegacyKeyToConfidentialStore() {
def legacy = Secret.legacyKey
def legacy = HistoricalSecrets.legacyKey
["Hello world","","\u0000unprintable"].each { str ->
def cipher = Secret.getCipher("AES");
cipher.init(Cipher.ENCRYPT_MODE, legacy);
def old = new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8"))))
def old = new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8"))))
def s = Secret.fromString(old)
assert s.plainText==str : "secret by the old key should decrypt"
assert s.encryptedValue!=old : "but when encrypting, ConfidentialKey should be in use"
......
......@@ -33,7 +33,7 @@ THE SOFTWARE.
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version>
<version>2.45-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Jenkins main module</name>
......@@ -181,7 +181,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>remoting</artifactId>
<version>3.4</version>
<version>3.4.1</version>
</dependency>
<dependency>
......
......@@ -28,7 +28,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version>
<version>2.45-SNAPSHOT</version>
</parent>
<artifactId>test</artifactId>
......
......@@ -519,39 +519,6 @@ public class AbstractProjectTest extends HudsonTestCase {
done.signal()
}
public void testRenameToPrivileged() {
def secret = jenkins.createProject(FreeStyleProject.class,"secret");
def regular = jenkins.createProject(FreeStyleProject.class,"regular")
jenkins.securityRealm = createDummySecurityRealm();
def auth = new ProjectMatrixAuthorizationStrategy();
jenkins.authorizationStrategy = auth;
auth.add(Jenkins.ADMINISTER, "alice");
auth.add(Jenkins.READ, "bob");
// bob the regular user can only see regular jobs
regular.addProperty(new AuthorizationMatrixProperty([(Job.READ) : ["bob"] as Set]));
def wc = createWebClient()
wc.login("bob")
wc.executeOnServer {
assert jenkins.getItem("secret")==null;
try {
regular.renameTo("secret")
fail("rename as an overwrite should have failed");
} catch (Exception e) {
// expected rename to fail in some non-descriptive generic way
e.printStackTrace()
}
}
// those two jobs should still be there
assert jenkins.getItem("regular")!=null;
assert jenkins.getItem("secret")!=null;
}
/**
* Trying to POST to config.xml by a different job type should fail.
*/
......
/*
* The MIT License
*
* Copyright 2016 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.console;
import hudson.MarkupText;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringWriter;
import java.util.logging.Level;
import org.apache.commons.io.Charsets;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.For;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.LoggerRule;
import org.kohsuke.stapler.framework.io.ByteBuffer;
@For({AnnotatedLargeText.class, ConsoleNote.class, ConsoleAnnotationOutputStream.class, PlainTextConsoleOutputStream.class})
public class AnnotatedLargeTextTest {
@ClassRule
public static JenkinsRule r = new JenkinsRule();
@Rule
public LoggerRule logging = new LoggerRule().record(ConsoleAnnotationOutputStream.class, Level.FINE).capture(100);
@Test
public void smokes() throws Exception {
ByteBuffer buf = new ByteBuffer();
PrintStream ps = new PrintStream(buf, true);
ps.print("Some text.\n");
ps.print("Go back to " + TestNote.encodeTo("/root", "your home") + ".\n");
ps.print("More text.\n");
AnnotatedLargeText<Void> text = new AnnotatedLargeText<>(buf, Charsets.UTF_8, true, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
text.writeLogTo(0, baos);
assertEquals("Some text.\nGo back to your home.\nMore text.\n", baos.toString());
StringWriter w = new StringWriter();
text.writeHtmlTo(0, w);
assertEquals("Some text.\nGo back to <a href='/root'>your home</a>.\nMore text.\n", w.toString());
}
@Issue("SECURITY-382")
@Test
public void oldDeserialization() throws Exception {
ByteBuffer buf = new ByteBuffer();
buf.write(("hello" + ConsoleNote.PREAMBLE_STR + "AAAAwR+LCAAAAAAAAP9dzLEOwVAUxvHThtiNprYxsGiMQhiwNSIhMR/tSZXr3Lr3oJPwPt7FM5hM3gFh8i3/5Bt+1yeUrYH6ap9Yza1Ys9WKWuMiR05wqWhEgpmyEy306Jxvwb19ccGNoBJjLplmgWq0xgOGCjkNZ2IyTrsRlFayVTs4gVMYqP3pw28/JnznuABF/rYWyIyeJfLQe1vxZiDQ7NnYZLn0UZGRRjA9MiV+0OyFv3+utadQyH8B+aJxVM4AAAA=" + ConsoleNote.POSTAMBLE_STR + "there\n").getBytes());
AnnotatedLargeText<Void> text = new AnnotatedLargeText<>(buf, Charsets.UTF_8, true, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
text.writeLogTo(0, baos);
assertEquals("hellothere\n", baos.toString());
StringWriter w = new StringWriter();
text.writeHtmlTo(0, w);
assertEquals("hellothere\n", w.toString());
assertThat(logging.getMessages(), hasItem("Failed to resurrect annotation")); // TODO assert that this is IOException: Refusing to deserialize unsigned note from an old log.
ConsoleNote.INSECURE = true;
try {
w = new StringWriter();
text.writeHtmlTo(0, w);
assertThat(w.toString(), containsString("<script>"));
} finally {
ConsoleNote.INSECURE = false;
}
}
@Issue("SECURITY-382")
@Test
public void badMac() throws Exception {
ByteBuffer buf = new ByteBuffer();
buf.write(("Go back to " + ConsoleNote.PREAMBLE_STR + "////4ByIhqPpAc43AbrEtyDUDc1/UEOXsoY6LeoHSeSlb1d7AAAAlR+LCAAAAAAAAP9b85aBtbiIQS+jNKU4P08vOT+vOD8nVc8xLy+/JLEkNcUnsSg9NSS1oiQktbhEBUT45ZekCpys9xWo8J3KxMDkycCWk5qXXpLhw8BcWpRTwiDkk5VYlqifk5iXrh9cUpSZl25dUcQghWaBM4QGGcYAAYxMDAwVBUAGZwkDq35Rfn4JABmN28qcAAAA" + ConsoleNote.POSTAMBLE_STR + "your home.\n").getBytes());
AnnotatedLargeText<Void> text = new AnnotatedLargeText<>(buf, Charsets.UTF_8, true, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
text.writeLogTo(0, baos);
assertEquals("Go back to your home.\n", baos.toString());
StringWriter w = new StringWriter();
text.writeHtmlTo(0, w);
assertEquals("Go back to your home.\n", w.toString());
assertThat(logging.getMessages(), hasItem("Failed to resurrect annotation")); // TODO assert that this is IOException: MAC mismatch
}
/** Simplified version of {@link HyperlinkNote}. */
static class TestNote extends ConsoleNote<Void> {
private final String url;
private final int length;
TestNote(String url, int length) {
this.url = url;
this.length = length;
}
@Override
public ConsoleAnnotator<?> annotate(Void context, MarkupText text, int charPos) {
text.addMarkup(charPos, charPos + length, "<a href='" + url + "'" + ">", "</a>");
return null;
}
static String encodeTo(String url, String text) throws IOException {
return new TestNote(url, text.length()).encode() + text;
}
}
}
......@@ -27,6 +27,7 @@ import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
......@@ -117,8 +118,7 @@ public class ConsoleAnnotatorTest {
// make sure raw console output doesn't include the garbage
TextPage raw = (TextPage)r.createWebClient().goTo(b.getUrl()+"consoleText","text/plain");
System.out.println(raw.getContent());
assertTrue(raw.getContent().contains("\nabc\ndef\n"));
assertThat(raw.getContent(), containsString("\nabc\ndef\n"));
}
......
package hudson.diagnosis;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import hudson.model.User;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import hudson.security.HudsonPrivateSecurityRealm;
import hudson.security.Permission;
import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContextHolder;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.xml.sax.SAXException;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
......@@ -13,6 +25,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import java.io.IOException;
import java.util.Collections;
/**
* @author Kohsuke Kawaguchi
......@@ -45,6 +58,48 @@ public class HudsonHomeDiskUsageMonitorTest {
}
}
@Issue("SECURITY-371")
@Test
public void noAccessForNonAdmin() throws Exception {
JenkinsRule.WebClient wc = j.createWebClient();
// TODO: Use MockAuthorizationStrategy in later versions
JenkinsRule.DummySecurityRealm realm = j.createDummySecurityRealm();
realm.addGroups("administrator", "admins");
realm.addGroups("bob", "users");
j.jenkins.setSecurityRealm(realm);
GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();
auth.add(Jenkins.ADMINISTER, "admins");
auth.add(Jenkins.READ, "users");
j.jenkins.setAuthorizationStrategy(auth);
WebRequest request = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull/act"), HttpMethod.POST);
NameValuePair param = new NameValuePair("no", "true");
request.setRequestParameters(Collections.singletonList(param));
HudsonHomeDiskUsageMonitor mon = HudsonHomeDiskUsageMonitor.get();
try {
wc.login("bob");
wc.getPage(request);
} catch (FailingHttpStatusCodeException e) {
assertEquals(403, e.getStatusCode());
}
assertTrue(mon.isEnabled());
try {
WebRequest getIndex = new WebRequest(wc.createCrumbedUrl("administrativeMonitor/hudsonHomeIsFull"), HttpMethod.GET);
wc.getPage(getIndex);
} catch (FailingHttpStatusCodeException e) {
assertEquals(403, e.getStatusCode());
}
wc.login("administrator");
wc.getPage(request);
assertFalse(mon.isEnabled());
}
/**
* Gets the warning form.
*/
......
......@@ -27,6 +27,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import hudson.security.ACL;
......@@ -49,6 +51,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -141,6 +144,26 @@ public class ComputerConfigDotXmlTest {
assertThat(updatedSlave.getNumExecutors(), equalTo(42));
}
@Test
@Issue("SECURITY-343")
public void emptyNodeMonitorDataWithoutConnect() throws Exception {
rule.jenkins.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy());
assertTrue(computer.getMonitorData().isEmpty());
}
@Test
@Issue("SECURITY-343")
public void populatedNodeMonitorDataWithConnect() throws Exception {
GlobalMatrixAuthorizationStrategy auth = new GlobalMatrixAuthorizationStrategy();
rule.jenkins.setAuthorizationStrategy(auth);
auth.add(Computer.CONNECT, "user");
assertFalse(computer.getMonitorData().isEmpty());
}
private OutputStream captureOutput() throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
......
......@@ -26,16 +26,19 @@ package hudson.model;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.xml.XmlPage;
import java.io.File;
import jenkins.model.Jenkins;
import hudson.slaves.DumbSlave;
import hudson.slaves.OfflineCause;
import org.junit.Before;
import org.junit.Rule;
......@@ -43,6 +46,7 @@ import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import org.jvnet.hudson.test.recipes.LocalData;
public class ComputerTest {
......@@ -87,4 +91,27 @@ public class ComputerTest {
containsString("Agent called ‘nodeA’ already exists"));
}
}
@Test
public void doNotShowUserDetailsInOfflineCause() throws Exception {
DumbSlave slave = j.createOnlineSlave();
final Computer computer = slave.toComputer();
computer.setTemporarilyOffline(true, new OfflineCause.UserCause(User.get("username"), "msg"));
verifyOfflineCause(computer);
}
@Test @LocalData
public void removeUserDetailsFromOfflineCause() throws Exception {
Computer computer = j.jenkins.getComputer("deserialized");
verifyOfflineCause(computer);
}
private void verifyOfflineCause(Computer computer) throws Exception {
XmlPage page = j.createWebClient().goToXml("computer/" + computer.getName() + "/config.xml");
String content = page.getWebResponse().getContentAsString("UTF-8");
assertThat(content, containsString("temporaryOfflineCause"));
assertThat(content, containsString("<userId>username</userId>"));
assertThat(content, not(containsString("ApiTokenProperty")));
assertThat(content, not(containsString("apiToken")));
}
}
......@@ -24,8 +24,26 @@
package hudson.model;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import hudson.AbortException;
import hudson.cli.CLICommand;
import hudson.cli.CLICommandInvoker;
import hudson.cli.CopyJobCommand;
import hudson.cli.CreateJobCommand;
import hudson.security.ACL;
import hudson.security.csrf.CrumbIssuer;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.apache.commons.httpclient.HttpStatus;
import org.junit.Test;
......@@ -35,6 +53,7 @@ import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;
import org.jvnet.hudson.test.MockFolder;
public class ItemsTest {
......@@ -100,5 +119,171 @@ public class ItemsTest {
assertFalse(new File(tmp, "foo/test/1").exists());
assertTrue(new File(tmp, "bar/test/1").exists());
}
// TODO would be more efficient to run these all as a single test case, but after a few Jetty seems to stop serving new content and new requests just hang.
private void overwriteTargetSetUp() throws Exception {
// A fully visible item:
r.createFreeStyleProject("visible").setDescription("visible");
// An item known to exist but not visible:
r.createFreeStyleProject("known").setDescription("known");
// An item not even known to exist:
r.createFreeStyleProject("secret").setDescription("secret");
// A folder from which to launch move attacks:
r.createFolder("d");
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
grant(Jenkins.READ).everywhere().to("attacker").
grant(Item.READ, Item.CONFIGURE, Item.CREATE, Item.DELETE).onPaths("(?!known|secret).*").to("attacker").
grant(Item.DISCOVER).onPaths("known").to("attacker"));
}
/** Control cases: if there is no such item yet, nothing is stopping you. */
@Test public void overwriteNonexistentTarget() throws Exception {
overwriteTargetSetUp();
for (OverwriteTactic tactic : OverwriteTactic.values()) {
tactic.run(r, "nonexistent");
System.out.println(tactic + " worked as expected on a nonexistent target");
r.jenkins.getItem("nonexistent").delete();
}
}
private void cannotOverwrite(String target) throws Exception {
overwriteTargetSetUp();
for (OverwriteTactic tactic : OverwriteTactic.values()) {
try {
tactic.run(r, target);
fail(tactic + " was not supposed to work against " + target);
} catch (Exception x) {
System.out.println("good, " + tactic + " failed on " + target + ": " + x);
assertEquals(tactic + " still overwrote " + target, target, r.jenkins.getItemByFullName(target, FreeStyleProject.class).getDescription());
}
}
}
/** More control cases: for non-security-sensitive scenarios, we prevent you from overwriting existing items. */
@Test public void overwriteVisibleTarget() throws Exception {
cannotOverwrite("visible");
}
/** You may not overwrite an item you know is there even if you cannot see it. */
@Test public void overwriteKnownTarget() throws Exception {
cannotOverwrite("known");
}
/** You are somehow prevented from overwriting an item even if you did not previously know it was there. */
@Issue("SECURITY-321")
@Test public void overwriteHiddenTarget() throws Exception {
cannotOverwrite("secret");
}
/** All known means of creating an item under a new name. */
private enum OverwriteTactic {
/** Use the REST command to create an empty project (normally used only from the UI in the New Item dialog). */
REST_EMPTY {
@Override void run(JenkinsRule r, String target) throws Exception {
JenkinsRule.WebClient wc = wc(r);
wc.getOptions().setRedirectEnabled(false);
wc.getOptions().setThrowExceptionOnFailingStatusCode(false); // redirect perversely counts as a failure
WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=hudson.model.FreeStyleProject"), HttpMethod.POST)).getWebResponse();
if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
throw new FailingHttpStatusCodeException(webResponse);
}
}
},
/** Use the REST command to copy an existing project (normally used from the UI in the New Item dialog). */
REST_COPY {
@Override void run(JenkinsRule r, String target) throws Exception {
r.createFreeStyleProject("dupe");
JenkinsRule.WebClient wc = wc(r);
wc.getOptions().setRedirectEnabled(false);
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target + "&mode=copy&from=dupe"), HttpMethod.POST)).getWebResponse();
r.jenkins.getItem("dupe").delete();
if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
throw new FailingHttpStatusCodeException(webResponse);
}
}
},
/** Overwrite target using REST command to create a project from XML submission. */
REST_CREATE {
@Override void run(JenkinsRule r, String target) throws Exception {
JenkinsRule.WebClient wc = wc(r);
WebRequest req = new WebRequest(createCrumbedUrl(r, wc, "createItem?name=" + target), HttpMethod.POST);
req.setAdditionalHeader("Content-Type", "application/xml");
req.setRequestBody("<project/>");
wc.getPage(req);
}
},
/** Overwrite target using REST command to rename an existing project (normally used from the UI in the Configure screen). */
REST_RENAME {
@Override void run(JenkinsRule r, String target) throws Exception {
r.createFreeStyleProject("dupe");
JenkinsRule.WebClient wc = wc(r);
wc.getOptions().setRedirectEnabled(false);
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
WebResponse webResponse = wc.getPage(new WebRequest(createCrumbedUrl(r, wc, "job/dupe/doRename?newName=" + target), HttpMethod.POST)).getWebResponse();
if (webResponse.getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) {
r.jenkins.getItem("dupe").delete();
throw new FailingHttpStatusCodeException(webResponse);
}
assertNull(r.jenkins.getItem("dupe"));
}
},
/** Overwrite target using the CLI {@code create-job} command. */
CLI_CREATE {
@Override void run(JenkinsRule r, String target) throws Exception {
CLICommand cmd = new CreateJobCommand();
CLICommandInvoker invoker = new CLICommandInvoker(r, cmd);
cmd.setTransportAuth(User.get("attacker").impersonate());
int status = invoker.withStdin(new ByteArrayInputStream("<project/>".getBytes("US-ASCII"))).invokeWithArgs(target).returnCode();
if (status != 0) {
throw new AbortException("CLI command failed with status " + status);
}
}
},
/** Overwrite target using the CLI {@code copy-job} command. */
CLI_COPY {
@Override void run(JenkinsRule r, String target) throws Exception {
r.createFreeStyleProject("dupe");
CLICommand cmd = new CopyJobCommand();
CLICommandInvoker invoker = new CLICommandInvoker(r, cmd);
cmd.setTransportAuth(User.get("attacker").impersonate());
int status = invoker.invokeWithArgs("dupe", target).returnCode();
r.jenkins.getItem("dupe").delete();
if (status != 0) {
throw new AbortException("CLI command failed with status " + status);
}
}
},
/** Overwrite target using a move function normally called from {@code cloudbees-folder} via a {@code move} action. */
MOVE {
@Override void run(JenkinsRule r, String target) throws Exception {
try {
SecurityContext orig = ACL.impersonate(User.get("attacker").impersonate());
try {
Items.move(r.jenkins.getItemByFullName("d", MockFolder.class).createProject(FreeStyleProject.class, target), r.jenkins);
} finally {
SecurityContextHolder.setContext(orig);
}
assertNull(r.jenkins.getItemByFullName("d/" + target));
} catch (Exception x) {
r.jenkins.getItemByFullName("d/" + target).delete();
throw x;
}
}
};
abstract void run(JenkinsRule r, String target) throws Exception;
private static final JenkinsRule.WebClient wc(JenkinsRule r) throws Exception {
return r.createWebClient().login("attacker");
}
// TODO replace with standard version once it is fixed to detect an existing query string
private static URL createCrumbedUrl(JenkinsRule r, JenkinsRule.WebClient wc, String relativePath) throws IOException {
CrumbIssuer issuer = r.jenkins.getCrumbIssuer();
String crumbName = issuer.getDescriptor().getCrumbRequestField();
String crumb = issuer.getCrumb(null);
return new URL(wc.getContextPath() + relativePath + (relativePath.contains("?") ? "&" : "?") + crumbName + "=" + crumb);
}
}
}
......@@ -35,6 +35,7 @@ import hudson.model.Queue.WaitingItem;
import hudson.model.labels.*;
import hudson.model.queue.CauseOfBlockage;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import hudson.security.HudsonPrivateSecurityRealm;
import hudson.security.Permission;
......@@ -53,6 +54,8 @@ import java.util.concurrent.Callable;
import jenkins.model.Jenkins;
import jenkins.security.QueueItemAuthenticatorConfiguration;
import org.acegisecurity.context.SecurityContextHolder;
import static org.hamcrest.core.StringEndsWith.endsWith;
import static org.junit.Assert.*;
import org.junit.Before;
......@@ -123,6 +126,32 @@ public class NodeTest {
assertNull(computer.getOfflineCause());
}
@Test
public void testOfflineCauseAsAnonymous() throws Exception {
Node node = j.createOnlineSlave();
final Computer computer = node.toComputer();
OfflineCause.UserCause cause;
try (ACLContext ctxt = ACL.as(Jenkins.ANONYMOUS)) {
computer.doToggleOffline("original message");
}
cause = (UserCause) computer.getOfflineCause();
assertThat(cause.toString(), endsWith("Disconnected by anonymous : original message"));
assertEquals(User.getUnknown(), cause.getUser());
final User root = User.get("root@localhost");
try (ACLContext ctxt = ACL.as(root.impersonate())) {
computer.doChangeOfflineCause("new message");
}
cause = (UserCause) computer.getOfflineCause();
assertThat(cause.toString(), endsWith("Disconnected by root@localhost : new message"));
assertEquals(root, cause.getUser());
computer.doToggleOffline(null);
assertNull(computer.getOfflineCause());
}
@Test
public void testGetLabelCloud() throws Exception {
Node node = j.createOnlineSlave();
......
package hudson.model;
import static org.junit.Assert.*;
import com.gargoylesoftware.htmlunit.html.DomNodeUtil;
import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlFormUtil;
import org.junit.Rule;
import org.junit.Test;
import com.gargoylesoftware.htmlunit.html.HtmlOption;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput;
import com.gargoylesoftware.htmlunit.html.HtmlOption;
import hudson.markup.MarkupFormatter;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpStatus;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.jvnet.hudson.test.CaptureEnvironmentBuilder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import java.util.Set;
/**
* @author huybrechts
*/
......@@ -28,6 +33,9 @@ public class ParametersTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Rule
public ErrorCollector collector = new ErrorCollector();
@Test
public void parameterTypes() throws Exception {
FreeStyleProject otherProject = j.createFreeStyleProject();
......@@ -216,4 +224,52 @@ public class ParametersTest {
final HtmlForm form = page.getFormByName("parameters");
HtmlFormUtil.submit(form, HtmlFormUtil.getButtonByCaption(form, "Build"));
}
@Issue("SECURITY-353")
@Test
public void xss() throws Exception {
j.jenkins.setMarkupFormatter(new MyMarkupFormatter());
FreeStyleProject p = j.createFreeStyleProject("p");
StringParameterDefinition param = new StringParameterDefinition("<param name>", "<param default>", "<param description>");
assertEquals("<b>[</b>param description<b>]</b>", param.getFormattedDescription());
p.addProperty(new ParametersDefinitionProperty(param));
WebClient wc = j.createWebClient();
wc.getOptions().setThrowExceptionOnFailingStatusCode(false);
HtmlPage page = wc.getPage(p, "build?delay=0sec");
collector.checkThat(page.getWebResponse().getStatusCode(), is(HttpStatus.SC_METHOD_NOT_ALLOWED)); // 405 to dissuade scripts from thinking this triggered the build
String text = page.getWebResponse().getContentAsString();
collector.checkThat("build page should escape param name", text, containsString("&lt;param name&gt;"));
collector.checkThat("build page should not leave param name unescaped", text, not(containsString("<param name>")));
collector.checkThat("build page should escape param default", text, containsString("&lt;param default&gt;"));
collector.checkThat("build page should not leave param default unescaped", text, not(containsString("<param default>")));
collector.checkThat("build page should mark up param description", text, containsString("<b>[</b>param description<b>]</b>"));
collector.checkThat("build page should not leave param description unescaped", text, not(containsString("<param description>")));
HtmlForm form = page.getFormByName("parameters");
HtmlTextInput value = form.getInputByValue("<param default>");
value.setText("<param value>");
j.submit(form);
j.waitUntilNoActivity();
FreeStyleBuild b = p.getBuildByNumber(1);
page = j.createWebClient().getPage(b, "parameters/");
text = page.getWebResponse().getContentAsString();
collector.checkThat("parameters page should escape param name", text, containsString("&lt;param name&gt;"));
collector.checkThat("parameters page should not leave param name unescaped", text, not(containsString("<param name>")));
collector.checkThat("parameters page should escape param value", text, containsString("&lt;param value&gt;"));
collector.checkThat("parameters page should not leave param value unescaped", text, not(containsString("<param value>")));
collector.checkThat("parameters page should mark up param description", text, containsString("<b>[</b>param description<b>]</b>"));
collector.checkThat("parameters page should not leave param description unescaped", text, not(containsString("<param description>")));
}
static class MyMarkupFormatter extends MarkupFormatter {
@Override
public void translate(String markup, Writer output) throws IOException {
Matcher m = Pattern.compile("[<>]").matcher(markup);
StringBuffer buf = new StringBuffer();
while (m.find()) {
m.appendReplacement(buf, m.group().equals("<") ? "<b>[</b>" : "<b>]</b>");
}
m.appendTail(buf);
output.write(buf.toString());
}
}
}
......@@ -32,14 +32,19 @@ import static org.junit.Assert.fail;
import hudson.model.FreeStyleProject;
import hudson.model.ListView;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import hudson.model.User;
import hudson.model.View;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.AuthorizationStrategy;
import hudson.security.GlobalMatrixAuthorizationStrategy;
import jenkins.model.Jenkins;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
......@@ -383,6 +388,37 @@ public class SearchTest {
assertTrue(suggest(j.jenkins.getSearchIndex(),"foo").contains(p));
}
@Issue("SECURITY-385")
@Test
public void testInaccessibleViews() throws IOException {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
GlobalMatrixAuthorizationStrategy strategy = new GlobalMatrixAuthorizationStrategy();
strategy.add(Jenkins.READ, "alice");
j.jenkins.setAuthorizationStrategy(strategy);
j.jenkins.addView(new ListView("foo", j.jenkins));
// SYSTEM can see all the views
assertEquals("two views exist", 2, Jenkins.getInstance().getViews().size());
List<SearchItem> results = new ArrayList<>();
j.jenkins.getSearchIndex().suggest("foo", results);
assertEquals("nonempty results list", 1, results.size());
// Alice can't
assertFalse("no permission", j.jenkins.getView("foo").getACL().hasPermission(User.get("alice").impersonate(), View.READ));
ACL.impersonate(User.get("alice").impersonate(), new Runnable() {
@Override
public void run() {
assertEquals("no visible views", 0, Jenkins.getInstance().getViews().size());
List<SearchItem> results = new ArrayList<>();
j.jenkins.getSearchIndex().suggest("foo", results);
assertEquals("empty results list", Collections.emptyList(), results);
}
});
}
@Test
public void testSearchWithinFolders() throws Exception {
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2016, 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.util;
import hudson.model.FreeStyleProject;
import hudson.model.ParameterDefinition;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.PasswordParameterDefinition;
import org.hamcrest.core.Is;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;
import java.io.IOException;
import java.util.regex.Pattern;
import static org.hamcrest.core.Is.isA;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.*;
/**
* Tests {@link Secret}.
*/
public class SecretCompatTest {
@Rule
public JenkinsRule j = new JenkinsRule() {
@Override
public void before() throws Throwable {
Secret.resetKeyForTest(); //As early as possible
super.before();
}
};
@After
public void after() {
Secret.resetKeyForTest();
}
@Test
@Issue("SECURITY-304")
public void encryptedValueStaysTheSameAfterRoundtrip() throws Exception {
FreeStyleProject project = j.createFreeStyleProject();
project.addProperty(new ParametersDefinitionProperty(new PasswordParameterDefinition("p", "s3cr37", "Keep this a secret")));
project.getAllActions(); // initialize Actionable.actions; otherwise first made nonnull while rendering sidepanel after redirect after round #1 has been saved, so only round #2 has <actions/>
project = j.configRoundtrip(project);
String round1 = project.getConfigFile().asString();
project = j.configRoundtrip(project);
String round2 = project.getConfigFile().asString();
assertEquals(round1, round2);
//But reconfiguring will make it a new value
project = j.jenkins.getItemByFullName(project.getFullName(), FreeStyleProject.class);
project.removeProperty(ParametersDefinitionProperty.class);
project.addProperty(new ParametersDefinitionProperty(new PasswordParameterDefinition("p", "s3cr37", "Keep this a secret")));
project = j.configRoundtrip(project);
String round3 = project.getConfigFile().asString();
assertNotEquals(round2, round3);
//Saving again will produce the same
project = j.configRoundtrip(project);
String round4 = project.getConfigFile().asString();
assertEquals(round3, round4);
}
@Test
@Issue("SECURITY-304")
@LocalData
public void canReadPreSec304Secrets() throws Exception {
FreeStyleProject project = j.jenkins.getItemByFullName("OldSecret", FreeStyleProject.class);
String oldxml = project.getConfigFile().asString();
//It should be unchanged on disk
assertThat(oldxml, containsString("<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>"));
ParametersDefinitionProperty property = project.getProperty(ParametersDefinitionProperty.class);
ParameterDefinition definition = property.getParameterDefinitions().get(0);
assertTrue(definition instanceof PasswordParameterDefinition);
Secret secret = ((PasswordParameterDefinition) definition).getDefaultValueAsSecret();
assertEquals("theSecret", secret.getPlainText());
//OK it was read correctly from disk, now the first roundtrip should update the encrypted value
project = j.configRoundtrip(project);
String newXml = project.getConfigFile().asString();
assertNotEquals(oldxml, newXml); //This could have changed because Jenkins has moved on, so not really a good check
assertThat(newXml, not(containsString("<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>")));
Pattern p = Pattern.compile("<defaultValue>\\{[A-Za-z0-9+/]+={0,2}}</defaultValue>");
assertTrue(p.matcher(newXml).find());
//But the next roundtrip should result in the same data
project = j.configRoundtrip(project);
String round2 = project.getConfigFile().asString();
assertEquals(newXml, round2);
}
}
package hudson.util;
import hudson.model.Items;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.when;
public class XStream2Security383Test {
@Rule
public JenkinsRule j = new JenkinsRule();
@Rule
public TemporaryFolder f = new TemporaryFolder();
@Mock
private StaplerRequest req;
@Mock
private StaplerResponse rsp;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
@Issue("SECURITY-383")
public void testXmlLoad() throws Exception {
File exploitFile = f.newFile();
try {
// be extra sure there's no file already
if (exploitFile.exists() && !exploitFile.delete()) {
throw new IllegalStateException("file exists and cannot be deleted");
}
File tempJobDir = new File(j.jenkins.getRootDir(), "security383");
String exploitXml = IOUtils.toString(
XStream2Security383Test.class.getResourceAsStream(
"/hudson/util/XStream2Security383Test/config.xml"), "UTF-8");
exploitXml = exploitXml.replace("@TOKEN@", exploitFile.getAbsolutePath());
FileUtils.write(new File(tempJobDir, "config.xml"), exploitXml);
try {
Items.load(j.jenkins, tempJobDir);
} catch (Exception e) {
// ignore
}
assertFalse("no file should be created here", exploitFile.exists());
} finally {
exploitFile.delete();
}
}
@Test
@Issue("SECURITY-383")
public void testPostJobXml() throws Exception {
File exploitFile = f.newFile();
try {
// be extra sure there's no file already
if (exploitFile.exists() && !exploitFile.delete()) {
throw new IllegalStateException("file exists and cannot be deleted");
}
File tempJobDir = new File(j.jenkins.getRootDir(), "security383");
String exploitXml = IOUtils.toString(
XStream2Security383Test.class.getResourceAsStream(
"/hudson/util/XStream2Security383Test/config.xml"), "UTF-8");
exploitXml = exploitXml.replace("@TOKEN@", exploitFile.getAbsolutePath());
when(req.getMethod()).thenReturn("POST");
when(req.getInputStream()).thenReturn(new Stream(IOUtils.toInputStream(exploitXml)));
when(req.getContentType()).thenReturn("application/xml");
when(req.getParameter("name")).thenReturn("foo");
try {
j.jenkins.doCreateItem(req, rsp);
} catch (Exception e) {
// don't care
}
assertFalse("no file should be created here", exploitFile.exists());
} finally {
exploitFile.delete();
}
}
private static class Stream extends ServletInputStream {
private final InputStream inner;
public Stream(final InputStream inner) {
this.inner = inner;
}
@Override
public int read() throws IOException {
return inner.read();
}
@Override
public boolean isFinished() {
throw new UnsupportedOperationException();
}
@Override
public boolean isReady() {
throw new UnsupportedOperationException();
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
}
......@@ -32,6 +32,7 @@ import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
......@@ -93,6 +94,28 @@ public class JenkinsTest {
@Rule public JenkinsRule j = new JenkinsRule();
@Issue("SECURITY-406")
@Test
public void testUserCreationFromUrlForAdmins() throws Exception {
WebClient wc = j.createWebClient();
assertNull("User not supposed to exist", User.getById("nonexistent", false));
wc.assertFails("user/nonexistent", 404);
assertNull("User not supposed to exist", User.getById("nonexistent", false));
try {
User.ALLOW_USER_CREATION_VIA_URL = true;
// expected to work
wc.goTo("user/nonexistent2");
assertNotNull("User supposed to exist", User.getById("nonexistent2", false));
} finally {
User.ALLOW_USER_CREATION_VIA_URL = false;
}
}
@Test
public void testIsDisplayNameUniqueTrue() throws Exception {
final String curJobName = "curJobName";
......
......@@ -11,6 +11,7 @@ import hudson.Util;
import hudson.util.Secret;
import hudson.util.SecretHelper;
import org.apache.commons.io.FileUtils;
import org.hamcrest.CoreMatchers;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.recipes.Recipe.Runner;
import org.xml.sax.SAXException;
......@@ -20,6 +21,9 @@ import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.regex.Pattern;
import static org.junit.Assert.assertThat;
/**
* @author Kohsuke Kawaguchi
......@@ -28,6 +32,8 @@ public class RekeySecretAdminMonitorTest extends HudsonTestCase {
@Inject
RekeySecretAdminMonitor monitor;
final String plain_regex_match = ".*\\{[A-Za-z0-9+/]+={0,2}}.*";
@Override
protected void setUp() throws Exception {
SecretHelper.set(TEST_KEY);
......@@ -76,8 +82,8 @@ public class RekeySecretAdminMonitorTest extends HudsonTestCase {
private void verifyRewrite(File dir) throws Exception {
File xml = new File(dir, "foo.xml");
assertEquals("<foo>" + encryptNew(TEST_KEY) + "</foo>".trim(),
FileUtils.readFileToString(xml).trim());
Pattern pattern = Pattern.compile("<foo>"+plain_regex_match+"</foo>");
assertTrue(pattern.matcher(FileUtils.readFileToString(xml).trim()).matches());
}
// TODO sometimes fails: "Invalid request submission: {json=[Ljava.lang.String;@2c46358e, .crumb=[Ljava.lang.String;@35661457}"
......
package jenkins.security;
import com.gargoylesoftware.htmlunit.Page;
import hudson.model.UnprotectedRootAction;
import hudson.security.ACL;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.util.HttpResponses;
import jenkins.model.Jenkins;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.HttpResponse;
public class Security380Test {
@Rule
public JenkinsRule j = new JenkinsRule();
@Issue("SECURITY-380")
@Test
public void testGetItemsWithoutAnonRead() throws Exception {
FullControlOnceLoggedInAuthorizationStrategy strategy = new FullControlOnceLoggedInAuthorizationStrategy();
strategy.setAllowAnonymousRead(false);
Jenkins.getInstance().setAuthorizationStrategy(strategy);
Jenkins.getInstance().setSecurityRealm(j.createDummySecurityRealm());
j.createFreeStyleProject();
ACL.impersonate(Jenkins.ANONYMOUS, new Runnable() {
@Override
public void run() {
Assert.assertEquals("no items", 0, Jenkins.getInstance().getItems().size());
}
});
}
@Issue("SECURITY-380")
@Test
public void testGetItems() throws Exception {
FullControlOnceLoggedInAuthorizationStrategy strategy = new FullControlOnceLoggedInAuthorizationStrategy();
strategy.setAllowAnonymousRead(true);
Jenkins.getInstance().setAuthorizationStrategy(strategy);
Jenkins.getInstance().setSecurityRealm(j.createDummySecurityRealm());
j.createFreeStyleProject();
ACL.impersonate(Jenkins.ANONYMOUS, new Runnable() {
@Override
public void run() {
Assert.assertEquals("one item", 1, Jenkins.getInstance().getItems().size());
}
});
}
@Issue("SECURITY-380")
@Test
public void testWithUnprotectedRootAction() throws Exception {
FullControlOnceLoggedInAuthorizationStrategy strategy = new FullControlOnceLoggedInAuthorizationStrategy();
strategy.setAllowAnonymousRead(false);
Jenkins.getInstance().setAuthorizationStrategy(strategy);
Jenkins.getInstance().setSecurityRealm(j.createDummySecurityRealm());
j.createFreeStyleProject();
JenkinsRule.WebClient wc = j.createWebClient();
Page page = wc.goTo("listJobs", "text/plain");
Assert.assertEquals("expect 0 items", "0", page.getWebResponse().getContentAsString().trim());
}
@TestExtension
public static class JobListingUnprotectedRootAction implements UnprotectedRootAction {
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getUrlName() {
return "listJobs";
}
public HttpResponse doIndex() throws Exception {
return HttpResponses.plainText(Integer.toString(Jenkins.getInstance().getItems().size()));
}
}
}
/*
* The MIT License
*
* Copyright 2016 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 jenkins.security.s2m;
import java.io.File;
import javax.inject.Inject;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
public class AdminFilePathFilterTest {
@Rule
public JenkinsRule r = new JenkinsRule();
@Inject
AdminWhitelistRule rule;
@Before
public void setUp() {
r.jenkins.getInjector().injectMembers(this);
rule.setMasterKillSwitch(false);
}
@Issue({"JENKINS-27055", "SECURITY-358"})
@Test
public void matchBuildDir() throws Exception {
File buildDir = r.buildAndAssertSuccess(r.createFreeStyleProject()).getRootDir();
assertTrue(rule.checkFileAccess("write", new File(buildDir, "whatever")));
assertFalse(rule.checkFileAccess("write", new File(buildDir, "build.xml")));
// WorkflowRun:
assertFalse(rule.checkFileAccess("write", new File(buildDir, "program.dat")));
assertFalse(rule.checkFileAccess("write", new File(buildDir, "workflow/23.xml")));
}
}
......@@ -22,7 +22,7 @@
* THE SOFTWARE.
*/
package jenkins.security;
package jenkins.security.s2m;
import hudson.FilePath;
import hudson.model.Slave;
......@@ -31,8 +31,6 @@ import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import jenkins.security.s2m.AdminWhitelistRule;
import jenkins.security.s2m.DefaultFilePathFilter;
import org.jenkinsci.remoting.RoleChecker;
import org.junit.Before;
import org.junit.Test;
......@@ -41,7 +39,6 @@ import org.junit.Rule;
import org.jvnet.hudson.test.JenkinsRule;
import javax.inject.Inject;
import org.jvnet.hudson.test.Issue;
public class DefaultFilePathFilterTest {
......@@ -112,11 +109,4 @@ public class DefaultFilePathFilterTest {
}
}
@Issue("JENKINS-27055")
@Test public void matchBuildDir() throws Exception {
File f = new File(r.buildAndAssertSuccess(r.createFreeStyleProject()).getRootDir(), "whatever");
rule.setMasterKillSwitch(false);
assertTrue(rule.checkFileAccess("write", f));
}
}
......@@ -43,10 +43,13 @@ import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.regex.Pattern;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.Issue;
......@@ -74,10 +77,18 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
@Extension
public static final class DescriptorImpl extends Descriptor<PasswordTest> {}
@Issue("SECURITY-266")
@Issue({"SECURITY-266", "SECURITY-304"})
public void testExposedCiphertext() throws Exception {
boolean saveEnabled = Item.EXTENDED_READ.getEnabled();
try {
//final String plain_regex_match = ".*\\{[A-Za-z0-9+/]+={0,2}}.*";
final String xml_regex_match = "\\{[A-Za-z0-9+/]+={0,2}}";
final Pattern xml_regex_pattern = Pattern.compile(xml_regex_match);
final String staticTest = "\n\nvalue=\"{AQAAABAAAAAgXhXgopokysZkduhl+v1gm0UhUBBbjKDVpKz7bGk3mIO53cNTRdlu7LC4jZYEc+vF}\"\n";
//Just a quick verification on what could be on the page and that the regexp is correctly set up
assertThat(xml_regex_pattern.matcher(staticTest).find(), is(true));
jenkins.setSecurityRealm(createDummySecurityRealm());
// TODO 1.645+ use MockAuthorizationStrategy
GlobalMatrixAuthorizationStrategy pmas = new GlobalMatrixAuthorizationStrategy();
......@@ -89,7 +100,7 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
pmas.add(Item.CREATE, "dev"); // so we can show CopyJobCommand would barf; more realistic would be to grant it only in a subfolder
jenkins.setAuthorizationStrategy(pmas);
Secret s = Secret.fromString("s3cr3t");
String sEnc = s.getEncryptedValue();
//String sEnc = s.getEncryptedValue();
FreeStyleProject p = createFreeStyleProject("p");
p.setDisplayName("Unicode here ←");
p.setDescription("This+looks+like+Base64+but+is+not+a+secret");
......@@ -98,14 +109,15 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
// Control case: an administrator can read and write configuration freely.
wc.login("admin");
HtmlPage configure = wc.getPage(p, "configure");
assertThat(configure.getWebResponse().getContentAsString(), containsString(sEnc));
assertThat(xml_regex_pattern.matcher(configure.getWebResponse().getContentAsString()).find(), is(true));
submit(configure.getFormByName("config"));
VulnerableProperty vp = p.getProperty(VulnerableProperty.class);
assertNotNull(vp);
assertEquals(s, vp.secret);
Page configXml = wc.goTo(p.getUrl() + "config.xml", "application/xml");
String xmlAdmin = configXml.getWebResponse().getContentAsString();
assertThat(xmlAdmin, containsString("<secret>" + sEnc + "</secret>"));
assertThat(Pattern.compile("<secret>" + xml_regex_match + "</secret>").matcher(xmlAdmin).find(), is(true));
assertThat(xmlAdmin, containsString("<displayName>" + p.getDisplayName() + "</displayName>"));
assertThat(xmlAdmin, containsString("<description>" + p.getDescription() + "</description>"));
// CLICommandInvoker does not work here, as it sets up its own SecurityRealm + AuthorizationStrategy.
......@@ -127,11 +139,11 @@ public class PasswordTest extends HudsonTestCase implements Describable<Password
// Test case: another user with EXTENDED_READ but not CONFIGURE should not get access even to encrypted secrets.
wc.login("dev");
configure = wc.getPage(p, "configure");
assertThat(configure.getWebResponse().getContentAsString(), not(containsString(sEnc)));
assertThat(xml_regex_pattern.matcher(configure.getWebResponse().getContentAsString()).find(), is(false));
configXml = wc.goTo(p.getUrl() + "config.xml", "application/xml");
String xmlDev = configXml.getWebResponse().getContentAsString();
assertThat(xmlDev, not(containsString(sEnc)));
assertEquals(xmlAdmin.replace(sEnc, "********"), xmlDev);
assertThat(xml_regex_pattern.matcher(xmlDev).find(), is(false));
assertEquals(xmlAdmin.replaceAll(xml_regex_match, "********"), xmlDev);
getJobCommand = new GetJobCommand();
Authentication devAuth = User.get("dev").impersonate();
getJobCommand.setTransportAuth(devAuth);
......
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
<disabledAdministrativeMonitors/>
<version>1.0</version>
<numExecutors>2</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
<securityRealm class="hudson.security.SecurityRealm$None"/>
<disableRememberMe>false</disableRememberMe>
<projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
<workspaceDir>${JENKINS_HOME}/workspace/${ITEM_FULLNAME}</workspaceDir>
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
<jdks/>
<viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
<myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
<clouds/>
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
<views>
<hudson.model.AllView>
<owner class="hudson" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
<primaryView>All</primaryView>
<slaveAgentPort>0</slaveAgentPort>
<label></label>
<nodeProperties/>
<globalNodeProperties/>
<noUsageStatistics>true</noUsageStatistics>
</hudson>
<?xml version='1.0' encoding='UTF-8'?>
<slave>
<temporaryOfflineCause class="hudson.slaves.OfflineCause$UserCause">
<timestamp>1479196265920</timestamp>
<description>
<holder>
<owner>hudson.slaves.Messages</owner>
</holder>
<key>SlaveComputer.DisconnectedBy</key>
<args>
<string>username</string>
<string> : msg</string>
</args>
</description>
<user>
<fullName>username</fullName>
<properties>
<jenkins.security.ApiTokenProperty>
<apiToken>wPfVKd4HGJzRoEpazbTu35nXXfI34cguPjm+5JPO7pZDFLFgpFLviQsS3NdJndax</apiToken>
</jenkins.security.ApiTokenProperty>
<hudson.tasks.Mailer_-UserProperty/>
<hudson.model.MyViewsProperty>
<views>
<hudson.model.AllView>
<owner class="hudson.model.MyViewsProperty" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
</hudson.model.MyViewsProperty>
<hudson.model.PaneStatusProperties>
<collapsed/>
</hudson.model.PaneStatusProperties>
<hudson.search.UserSearchProperty>
<insensitiveSearch>false</insensitiveSearch>
</hudson.search.UserSearchProperty>
</properties>
</user>
</temporaryOfflineCause>
<name>deserialized</name>
<description>dummy</description>
<remoteFS>...</remoteFS>
<numExecutors>1</numExecutors>
<mode>NORMAL</mode>
<retentionStrategy class="hudson.slaves.RetentionStrategy$2">
<DESCRIPTOR>
<outer-class reference="../.."/>
</DESCRIPTOR>
</retentionStrategy>
<launcher/>
<label></label>
<nodeProperties/>
<userId>SYSTEM</userId>
</slave>
<?xml version='1.0' encoding='UTF-8'?>
<user>
<fullName>username</fullName>
<id>username</id>
<description></description>
<properties>
<jenkins.security.ApiTokenProperty>
<apiToken>qykj8q6EqvMg9LPu+lCqLiXBZvEVdCTWoYJwmicXgH+yh1ZUm85iHe29grd+g3QG</apiToken>
</jenkins.security.ApiTokenProperty>
<com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty plugin="credentials@1.18">
<domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash"/>
</com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty>
<hudson.tasks.Mailer_-UserProperty>
<emailAddress></emailAddress>
</hudson.tasks.Mailer_-UserProperty>
<hudson.model.MyViewsProperty>
<primaryViewName></primaryViewName>
<views>
<hudson.model.AllView>
<owner class="hudson.model.MyViewsProperty" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
</hudson.model.MyViewsProperty>
<hudson.model.PaneStatusProperties>
<collapsed/>
</hudson.model.PaneStatusProperties>
<org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
<authorizedKeys></authorizedKeys>
</org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl>
<hudson.search.UserSearchProperty>
<insensitiveSearch>false</insensitiveSearch>
</hudson.search.UserSearchProperty>
</properties>
</user>
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
<disabledAdministrativeMonitors/>
<version>1.625.4-SNAPSHOT (private-12/16/2016 18:04 GMT-rsandell)</version>
<numExecutors>2</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
<securityRealm class="hudson.security.SecurityRealm$None"/>
<disableRememberMe>false</disableRememberMe>
<projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
<workspaceDir>${ITEM_ROOTDIR}/workspace</workspaceDir>
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
<jdks/>
<viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
<myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
<clouds/>
<quietPeriod>5</quietPeriod>
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
<views>
<hudson.model.AllView>
<owner class="hudson" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
<primaryView>All</primaryView>
<slaveAgentPort>0</slaveAgentPort>
<label></label>
<nodeProperties/>
<globalNodeProperties/>
</hudson>
\ No newline at end of file
<?xml version='1.0' encoding='UTF-8'?>
<project>
<actions/>
<description></description>
<keepDependencies>false</keepDependencies>
<properties>
<hudson.model.ParametersDefinitionProperty>
<parameterDefinitions>
<hudson.model.PasswordParameterDefinition>
<name>alice</name>
<description>theSecret</description>
<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>
</hudson.model.PasswordParameterDefinition>
</parameterDefinitions>
</hudson.model.ParametersDefinitionProperty>
</properties>
<scm class="hudson.scm.NullSCM"/>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers/>
<concurrentBuild>false</concurrentBuild>
<builders/>
<publishers/>
<buildWrappers/>
</project>
\ No newline at end of file
4311ab5e95e3da7b9b1360b52cac6f0f666db7b48f9f701296cc07c8f00612b451f1874e584d49560810619e8a6ff6b19f8f58ae1305c515fc62a7b60ea3a69e6058cad16b2c8df317952b749fdaaecab013431da55bb4ea4b8eee754fa043261b51a99a2b537fd57f867cdcb1e209f3bba735a8672dbfc3f10b0e2209a81683
\ No newline at end of file
<set>
<hudson.util.RunList>
<base class="java.util.ServiceLoader">
<providers/>
<lookupIterator>
<configs class="java.util.Collections$3">
<i class="java.util.AbstractList$Itr">
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class="java.util.Arrays$ArrayList">
<a class="string-array">
<string>foo</string>
</a>
</outer-class>
</i>
<val_-c class="java.util.Arrays$ArrayList" reference="../i/outer-class"/>
</configs>
<pending class="javax.imageio.spi.FilterIterator">
<iter class="java.util.AbstractList$Itr">
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class="java.util.Arrays$ArrayList">
<a>
<java.lang.ProcessBuilder>
<command>
<string>touch</string>
<string>@TOKEN@</string>
</command>
<redirectErrorStream>false</redirectErrorStream>
</java.lang.ProcessBuilder>
</a>
</outer-class>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="java.util.HashMap$Node">
<hash>101575</hash>
<key class="string">foo</key>
<value class="string">foo</value>
</next>
</pending>
<outer-class reference="../.."/>
</lookupIterator>
</base>
</hudson.util.RunList>
</set>
\ No newline at end of file
......@@ -28,7 +28,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>2.44-SNAPSHOT</version>
<version>2.45-SNAPSHOT</version>
</parent>
<artifactId>jenkins-war</artifactId>
......
......@@ -2355,6 +2355,7 @@ function createSearchBox(searchURL) {
var ac = new YAHOO.widget.AutoComplete("search-box","search-box-completion",ds);
ac.typeAhead = false;
ac.autoHighlight = false;
ac.formatResult = ac.formatEscapedResult;
var box = $("search-box");
var sizer = $("search-box-sizer");
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册