, ModelObjectWithContextMenu {
-
+
+ /**
+ * The username of the 'unknown' user used to avoid null user references.
+ */
+ private static final String UKNOWN_USERNAME = "unknown";
+
+ /**
+ * These usernames should not be used by real users logging into Jenkins. Therefore, we prevent
+ * users with these names from being saved.
+ */
+ private static final String[] ILLEGAL_PERSISTED_USERNAMES = new String[]{ACL.ANONYMOUS_USERNAME,
+ ACL.SYSTEM_USERNAME, UKNOWN_USERNAME};
private transient final String id;
private volatile String fullName;
@@ -320,7 +332,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
* This is used to avoid null {@link User} instance.
*/
public static @Nonnull User getUnknown() {
- return get("unknown");
+ return get(UKNOWN_USERNAME);
}
/**
@@ -642,10 +654,35 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
return new File(Jenkins.getInstance().getRootDir(), "users");
}
+ /**
+ * Is the ID allowed? Some are prohibited for security reasons. See SECURITY-166.
+ *
+ * Note that this is only enforced when saving. These users are often created
+ * via the constructor (and even listed on /asynchPeople), but our goal is to
+ * prevent anyone from logging in as these users. Therefore, we prevent
+ * saving a User with one of these ids.
+ *
+ * @return true if the username or fullname is valid
+ * @since 1.600
+ */
+ public static boolean isIdOrFullnameAllowed(String id) {
+ for (String invalidId : ILLEGAL_PERSISTED_USERNAMES) {
+ if (id.equalsIgnoreCase(invalidId))
+ return false;
+ }
+ return true;
+ }
+
/**
* Save the settings to a file.
*/
- public synchronized void save() throws IOException {
+ public synchronized void save() throws IOException, FormValidation {
+ if (! isIdOrFullnameAllowed(id)) {
+ throw FormValidation.error(Messages.User_IllegalUsername(id));
+ }
+ if (! isIdOrFullnameAllowed(fullName)) {
+ throw FormValidation.error(Messages.User_IllegalFullname(fullName));
+ }
if(BulkChange.contains(this)) return;
getConfigFile().write(this);
SaveableListener.fireOnChange(this, getConfigFile());
diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java
index d03727af6cf4a31a9743576756e479947c3776d1..d30a80e876fdf0d4269affa2ec321147c6c52a73 100644
--- a/core/src/main/java/hudson/model/View.java
+++ b/core/src/main/java/hudson/model/View.java
@@ -58,6 +58,8 @@ import hudson.widgets.Widget;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithChildren;
import jenkins.util.ProgressiveRendering;
+import jenkins.util.xml.XMLUtils;
+
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
@@ -75,9 +77,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Source;
-import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.BufferedInputStream;
@@ -106,6 +106,7 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jenkins.model.Jenkins.*;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.xml.sax.SAXException;
/**
* Encapsulates the rendering of the list of {@link TopLevelItem}s
@@ -1063,7 +1064,10 @@ public abstract class View extends AbstractModelObject implements AccessControll
}
/**
- * Updates Job by its XML definition.
+ * Updates the View with the new XML definition.
+ * @param source source of the Item's new definition.
+ * The source should be either a StreamSource
or SAXSource
, other sources
+ * may not be handled.
*/
public void updateByXml(Source source) throws IOException {
checkPermission(CONFIGURE);
@@ -1072,13 +1076,12 @@ public abstract class View extends AbstractModelObject implements AccessControll
// this allows us to use UTF-8 for storing data,
// plus it checks any well-formedness issue in the submitted
// data
- Transformer t = TransformerFactory.newInstance()
- .newTransformer();
- t.transform(source,
- new StreamResult(out));
+ XMLUtils.safeTransform(source, new StreamResult(out));
out.close();
} catch (TransformerException e) {
throw new IOException("Failed to persist configuration.xml", e);
+ } catch (SAXException e) {
+ throw new IOException("Failed to persist configuration.xml", e);
}
// try to reflect the changes by reloading
diff --git a/core/src/main/java/hudson/security/ACL.java b/core/src/main/java/hudson/security/ACL.java
index 2a1b230f5fcd2625965b320f2d1ea9499ae73152..f66fc863c3bff359ebac047ebae3ea20d21eed10 100644
--- a/core/src/main/java/hudson/security/ACL.java
+++ b/core/src/main/java/hudson/security/ACL.java
@@ -35,6 +35,8 @@ import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.acls.sid.PrincipalSid;
import org.acegisecurity.acls.sid.Sid;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Gate-keeper that controls access to Hudson's model objects.
@@ -95,23 +97,34 @@ public abstract class ACL {
}
};
+ /**
+ * The username for the anonymous user
+ */
+ @Restricted(NoExternalUse.class)
+ public static final String ANONYMOUS_USERNAME = "anonymous";
/**
* {@link Sid} that represents the anonymous unauthenticated users.
*
* {@link HudsonFilter} sets this up, so this sid remains the same
* regardless of the current {@link SecurityRealm} in use.
*/
- public static final Sid ANONYMOUS = new PrincipalSid("anonymous");
+ public static final Sid ANONYMOUS = new PrincipalSid(ANONYMOUS_USERNAME);
protected static final Sid[] AUTOMATIC_SIDS = new Sid[]{EVERYONE,ANONYMOUS};
+ /**
+ * The username for the system user
+ */
+ @Restricted(NoExternalUse.class)
+ public static final String SYSTEM_USERNAME = "SYSTEM";
+
/**
* {@link Sid} that represents the Hudson itself.
*
* This is used when Hudson is performing computation for itself, instead
* of acting on behalf of an user, such as doing builds.
*/
- public static final Authentication SYSTEM = new UsernamePasswordAuthenticationToken("SYSTEM","SYSTEM");
+ public static final Authentication SYSTEM = new UsernamePasswordAuthenticationToken(SYSTEM_USERNAME,"SYSTEM");
/**
* Changes the {@link Authentication} associated with the current thread
diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
index 642ebac84b2009e9228beebe1748881c40875819..546d611fcff0718083e0514d956ea481ca13318b 100644
--- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
+++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
@@ -337,6 +337,14 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
if(si.email==null || !si.email.contains("@"))
si.errorMessage = Messages.HudsonPrivateSecurityRealm_CreateAccount_InvalidEmailAddress();
+ if (! User.isIdOrFullnameAllowed(si.username)) {
+ si.errorMessage = hudson.model.Messages.User_IllegalUsername(si.username);
+ }
+
+ if (! User.isIdOrFullnameAllowed(si.fullname)) {
+ si.errorMessage = hudson.model.Messages.User_IllegalFullname(si.fullname);
+ }
+
if(si.errorMessage!=null) {
// failed. ask the user to try again.
req.setAttribute("data",si);
diff --git a/core/src/main/java/jenkins/fun/Jenkins100K.java b/core/src/main/java/jenkins/fun/Jenkins100K.java
new file mode 100644
index 0000000000000000000000000000000000000000..cad0e8aeed8ca72dfb526f114cb7d3aad426faa1
--- /dev/null
+++ b/core/src/main/java/jenkins/fun/Jenkins100K.java
@@ -0,0 +1,51 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2015, 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.fun;
+
+import hudson.Extension;
+import hudson.model.UnprotectedRootAction;
+
+/**
+ * Easter egg for Jenkins 100K certificate.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.600
+ */
+@Extension(ordinal=-100)
+public class Jenkins100K implements UnprotectedRootAction {
+ @Override
+ public String getIconFileName() {
+ return "/images/headshot.png";
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Jenkins 100K";
+ }
+
+ @Override
+ public String getUrlName() {
+ return "jenkins100k";
+ }
+}
diff --git a/core/src/main/java/jenkins/model/DownloadSettings.java b/core/src/main/java/jenkins/model/DownloadSettings.java
index 0cf410c28b662aea2ff5bb44e8a961c92dd630b1..e16898ab86dc83dc6816f31c744f860c91376e8c 100644
--- a/core/src/main/java/jenkins/model/DownloadSettings.java
+++ b/core/src/main/java/jenkins/model/DownloadSettings.java
@@ -25,6 +25,8 @@
package jenkins.model;
import hudson.Extension;
+import hudson.Main;
+import hudson.model.AdministrativeMonitor;
import hudson.model.AsyncPeriodicWork;
import hudson.model.DownloadService;
import hudson.model.TaskListener;
@@ -32,6 +34,7 @@ import hudson.model.UpdateSite;
import hudson.util.FormValidation;
import java.io.IOException;
import net.sf.json.JSONObject;
+import org.acegisecurity.AccessDeniedException;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
@@ -49,7 +52,7 @@ import org.kohsuke.stapler.StaplerRequest;
return Jenkins.getInstance().getInjector().getInstance(DownloadSettings.class);
}
- private boolean useBrowser = true; // historical default, not necessarily recommended
+ private boolean useBrowser = false;
public DownloadSettings() {
load();
@@ -69,6 +72,21 @@ import org.kohsuke.stapler.StaplerRequest;
save();
}
+ @Override public GlobalConfigurationCategory getCategory() {
+ return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
+ }
+
+ public static boolean usePostBack() {
+ return get().isUseBrowser() && Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER);
+ }
+
+ public static void checkPostBackAccess() throws AccessDeniedException {
+ if (!get().isUseBrowser()) {
+ throw new AccessDeniedException("browser-based download disabled");
+ }
+ Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
+ }
+
@Extension public static final class DailyCheck extends AsyncPeriodicWork {
public DailyCheck() {
@@ -79,10 +97,24 @@ import org.kohsuke.stapler.StaplerRequest;
return DAY;
}
+ @Override public long getInitialDelay() {
+ return Main.isUnitTest ? DAY : 0;
+ }
+
@Override protected void execute(TaskListener listener) throws IOException, InterruptedException {
if (get().isUseBrowser()) {
return;
}
+ boolean due = false;
+ for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) {
+ if (site.isDue()) {
+ due = true;
+ break;
+ }
+ }
+ if (!due) {
+ return;
+ }
HttpResponse rsp = Jenkins.getInstance().getPluginManager().doCheckUpdatesServer();
if (rsp instanceof FormValidation) {
listener.error(((FormValidation) rsp).renderHtml());
@@ -91,4 +123,12 @@ import org.kohsuke.stapler.StaplerRequest;
}
+ @Extension public static final class Warning extends AdministrativeMonitor {
+
+ @Override public boolean isActivated() {
+ return DownloadSettings.get().isUseBrowser();
+ }
+
+ }
+
}
diff --git a/core/src/main/java/jenkins/util/JSONSignatureValidator.java b/core/src/main/java/jenkins/util/JSONSignatureValidator.java
index b36f4e399f8f1a1f2d2be0c0e1c1acdac620a03b..76d422cd90743110d7279db0ecb926efc6d10acc 100644
--- a/core/src/main/java/jenkins/util/JSONSignatureValidator.java
+++ b/core/src/main/java/jenkins/util/JSONSignatureValidator.java
@@ -135,6 +135,9 @@ public class JSONSignatureValidator {
// which isn't useful at all
Set anchors = new HashSet(); // CertificateUtil.getDefaultRootCAs();
Jenkins j = Jenkins.getInstance();
+ if (j == null) {
+ return anchors;
+ }
for (String cert : (Set) j.servletContext.getResourcePaths("/WEB-INF/update-center-rootCAs")) {
if (cert.endsWith("/") || cert.endsWith(".txt")) {
continue; // skip directories also any text files that are meant to be documentation
diff --git a/core/src/main/java/jenkins/util/VirtualFile.java b/core/src/main/java/jenkins/util/VirtualFile.java
index 3ccd3f09c69a2caad4878ee31f47059eea4de0a0..caf9bd0123a19cdda6d7d8303aaa78d18b7e6fec 100644
--- a/core/src/main/java/jenkins/util/VirtualFile.java
+++ b/core/src/main/java/jenkins/util/VirtualFile.java
@@ -33,12 +33,15 @@ import hudson.util.DirScanner;
import hudson.util.FileVisitor;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import javax.annotation.Nonnull;
import jenkins.MasterToSlaveFileCallable;
@@ -209,12 +212,14 @@ public abstract class VirtualFile implements Comparable, Serializab
* @return a wrapper
*/
public static VirtualFile forFile(final File f) {
- return new FileVF(f);
+ return new FileVF(f, f);
}
private static final class FileVF extends VirtualFile {
private final File f;
- FileVF(File f) {
+ private final File root;
+ FileVF(File f, File root) {
this.f = f;
+ this.root = root;
}
@Override public String getName() {
return f.getName();
@@ -223,46 +228,85 @@ public abstract class VirtualFile implements Comparable, Serializab
return f.toURI();
}
@Override public VirtualFile getParent() {
- return forFile(f.getParentFile());
+ return new FileVF(f.getParentFile(), root);
}
@Override public boolean isDirectory() throws IOException {
+ if (isIllegalSymlink()) {
+ return false;
+ }
return f.isDirectory();
}
@Override public boolean isFile() throws IOException {
+ if (isIllegalSymlink()) {
+ return false;
+ }
return f.isFile();
}
@Override public boolean exists() throws IOException {
+ if (isIllegalSymlink()) {
+ return false;
+ }
return f.exists();
}
@Override public VirtualFile[] list() throws IOException {
+ if (isIllegalSymlink()) {
+ return new VirtualFile[0];
+ }
File[] kids = f.listFiles();
if (kids == null) {
return new VirtualFile[0];
}
VirtualFile[] vfs = new VirtualFile[kids.length];
for (int i = 0; i < kids.length; i++) {
- vfs[i] = forFile(kids[i]);
+ vfs[i] = new FileVF(kids[i], root);
}
return vfs;
}
@Override public String[] list(String glob) throws IOException {
+ if (isIllegalSymlink()) {
+ return new String[0];
+ }
return new Scanner(glob).invoke(f, null);
}
@Override public VirtualFile child(String name) {
- return forFile(new File(f, name));
+ return new FileVF(new File(f, name), root);
}
@Override public long length() throws IOException {
+ if (isIllegalSymlink()) {
+ return 0;
+ }
return f.length();
}
@Override public long lastModified() throws IOException {
+ if (isIllegalSymlink()) {
+ return 0;
+ }
return f.lastModified();
}
@Override public boolean canRead() throws IOException {
+ if (isIllegalSymlink()) {
+ return false;
+ }
return f.canRead();
}
@Override public InputStream open() throws IOException {
+ if (isIllegalSymlink()) {
+ throw new FileNotFoundException(f.getPath());
+ }
return new FileInputStream(f);
}
+ private boolean isIllegalSymlink() { // TODO JENKINS-26838
+ try {
+ String myPath = f.getCanonicalPath();
+ String rootPath = root.getCanonicalPath();
+ if (!myPath.equals(rootPath) && !myPath.startsWith(rootPath + File.separatorChar)) {
+ return true;
+ }
+ } catch (IOException x) {
+ Logger.getLogger(VirtualFile.class.getName()).log(Level.FINE, "could not determine symlink status of " + f, x);
+ }
+ return false;
+ }
}
/**
diff --git a/core/src/main/java/jenkins/util/xml/FilteredFunctionContext.java b/core/src/main/java/jenkins/util/xml/FilteredFunctionContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..81048283deeef91736121a825aacaaade8599254
--- /dev/null
+++ b/core/src/main/java/jenkins/util/xml/FilteredFunctionContext.java
@@ -0,0 +1,77 @@
+
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2015, CloudBees, Inc. All rights reserved.
+ *
+ * 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.util.xml;
+
+import org.jaxen.Function;
+import org.jaxen.FunctionContext;
+import org.jaxen.UnresolvableException;
+import org.jaxen.XPathFunctionContext;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * {@link org.jaxen.FunctionContext} that removes some {@link org.dom4j.XPath}
+ * function names that are deemed bad as user input.
+ *
+ * @author Robert Sandell <rsandell@cloudbees.com>.
+ * @see org.jaxen.FunctionContext
+ * @see org.dom4j.XPath
+ * @see hudson.model.Api
+ */
+@Restricted(NoExternalUse.class)
+public class FilteredFunctionContext implements FunctionContext {
+
+ /**
+ * Default set of "bad" function names.
+ */
+ private static final Set DEFAULT_ILLEGAL_FUNCTIONS = Collections.unmodifiableSet(new HashSet(
+ Arrays.asList("document")
+ ));
+ private final FunctionContext base;
+ private final Set illegalFunctions;
+
+ public FilteredFunctionContext(Set illegalFunctions) {
+ this.illegalFunctions = illegalFunctions;
+ base = XPathFunctionContext.getInstance();
+ }
+
+ public FilteredFunctionContext() {
+ this(DEFAULT_ILLEGAL_FUNCTIONS);
+ }
+
+ @Override
+ public Function getFunction(String namespaceURI, String prefix, String localName) throws UnresolvableException {
+ if (localName != null && illegalFunctions.contains(localName.toLowerCase(Locale.ENGLISH))) {
+ throw new UnresolvableException("Illegal function: " + localName);
+ }
+ return base.getFunction(namespaceURI, prefix, localName);
+ }
+}
diff --git a/core/src/main/java/jenkins/util/xml/RestrictiveEntityResolver.java b/core/src/main/java/jenkins/util/xml/RestrictiveEntityResolver.java
new file mode 100644
index 0000000000000000000000000000000000000000..1cf37c9bd5393cad4ac06b5726df6fc6bb6ffd1a
--- /dev/null
+++ b/core/src/main/java/jenkins/util/xml/RestrictiveEntityResolver.java
@@ -0,0 +1,32 @@
+package jenkins.util.xml;
+
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+/**
+ * An EntityResolver that will fail to resolve any entities.
+ * Useful in preventing External XML Entity injection attacks.
+ */
+@Restricted(NoExternalUse.class)
+public final class RestrictiveEntityResolver implements EntityResolver {
+
+ public final static RestrictiveEntityResolver INSTANCE = new RestrictiveEntityResolver();
+
+ private RestrictiveEntityResolver() {
+ // prevent multiple instantiation.
+ super();
+ }
+
+ /**
+ * Throws a SAXException if this tried to resolve any entity.
+ */
+ @Override
+ public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
+ throw new SAXException("Refusing to resolve entity with publicId(" + publicId + ") and systemId (" + systemId + ")");
+ }
+}
diff --git a/core/src/main/java/jenkins/util/xml/XMLUtils.java b/core/src/main/java/jenkins/util/xml/XMLUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..84e6f6151bf6feba4b8589cb3ee4a8bea4d4120d
--- /dev/null
+++ b/core/src/main/java/jenkins/util/xml/XMLUtils.java
@@ -0,0 +1,99 @@
+package jenkins.util.xml;
+
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import javax.annotation.Nonnull;
+import javax.xml.XMLConstants;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.sax.SAXTransformerFactory;
+
+/**
+ * Utilities useful when working with various XML types.
+ */
+@Restricted(NoExternalUse.class)
+public final class XMLUtils {
+
+ private final static Logger LOGGER = LogManager.getLogManager().getLogger(XMLUtils.class.getName());
+ private final static String DISABLED_PROPERTY_NAME = XMLUtils.class.getName() + ".disableXXEPrevention";
+
+ /**
+ * Transform the source to the output in a manner that is protected against XXE attacks.
+ * If the transform can not be completed safely then an IOException is thrown.
+ * Note - to turn off safety set the system property disableXXEPrevention
to true
.
+ * @param source The XML input to transform. - This should be a StreamSource
or a
+ * SAXSource
in order to be able to prevent XXE attacks.
+ * @param out The Result of transforming the source
.
+ */
+ public static void safeTransform(@Nonnull Source source, @Nonnull Result out) throws TransformerException,
+ SAXException {
+
+ InputSource src = SAXSource.sourceToInputSource(source);
+ if (src != null) {
+ SAXTransformerFactory stFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
+ stFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+
+ XMLReader xmlReader = XMLReaderFactory.createXMLReader();
+ try {
+ xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ }
+ catch (SAXException ignored) { /* ignored */ }
+ try {
+ xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ }
+ catch (SAXException ignored) { /* ignored */ }
+ // defend against XXE
+ // the above features should strip out entities - however the feature may not be supported depending
+ // on the xml implementation used and this is out of our control.
+ // So add a fallback plan if all else fails.
+ xmlReader.setEntityResolver(RestrictiveEntityResolver.INSTANCE);
+ SAXSource saxSource = new SAXSource(xmlReader, src);
+ _transform(saxSource, out);
+ }
+ else {
+ // for some reason we could not convert source
+ // this applies to DOMSource and StAXSource - and possibly 3rd party implementations...
+ // a DOMSource can already be compromised as it is parsed by the time it gets to us.
+ if (Boolean.getBoolean(DISABLED_PROPERTY_NAME)) {
+ LOGGER.log(Level.WARNING, "XML external entity (XXE) prevention has been disabled by the system " +
+ "property {0}=true Your system may be vulnerable to XXE attacks.", DISABLED_PROPERTY_NAME);
+ if (LOGGER.isLoggable(Level.FINE)) {
+ LOGGER.log(Level.FINE, "Caller stack trace: ", new Exception("XXE Prevention caller history"));
+ }
+ _transform(source, out);
+ }
+ else {
+ throw new TransformerException("Could not convert source of type " + source.getClass() + " and " +
+ "XXEPrevention is enabled.");
+ }
+ }
+ }
+
+ /**
+ * potentially unsafe XML transformation.
+ * @param source The XML input to transform.
+ * @param out The Result of transforming the source
.
+ */
+ private static void _transform(Source source, Result out) throws TransformerException {
+ TransformerFactory factory = TransformerFactory.newInstance();
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+
+ // this allows us to use UTF-8 for storing data,
+ // plus it checks any well-formedness issue in the submitted data.
+ Transformer t = factory.newTransformer();
+ t.transform(source, out);
+ }
+
+}
diff --git a/core/src/main/resources/hudson/model/Messages.properties b/core/src/main/resources/hudson/model/Messages.properties
index 33dfb245cdfe42c0247e909407e7b4a8dbc477ee..16293724120d304adc87d13629a6db4f1ab9b5f3 100644
--- a/core/src/main/resources/hudson/model/Messages.properties
+++ b/core/src/main/resources/hudson/model/Messages.properties
@@ -356,3 +356,6 @@ Jenkins.CheckDisplayName.DisplayNameNotUniqueWarning=The display name, "{0}", is
Jenkins.NotAllowedName="{0}" is not allowed name
Jenkins.IsRestarting=Jenkins is restarting
+
+User.IllegalUsername="{0}" is prohibited as a username for security reasons.
+User.IllegalFullname="{0}" is prohibited as a full name for security reasons.
diff --git a/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly b/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly
index 777f1192c77f7b3216ceb9c42a866c432ad58083..684bc26bb054257e39347db2cd2415503f5ce9ae 100644
--- a/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly
+++ b/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly
@@ -31,8 +31,8 @@ THE SOFTWARE.
-->
-
-
+
+
+
+
+
+
diff --git a/core/src/main/resources/jenkins/model/DownloadSettings/Warning/message.jelly b/core/src/main/resources/jenkins/model/DownloadSettings/Warning/message.jelly
new file mode 100644
index 0000000000000000000000000000000000000000..8e1ee79cf8a68b9265371212393d4ef77b389adc
--- /dev/null
+++ b/core/src/main/resources/jenkins/model/DownloadSettings/Warning/message.jelly
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/jenkins/model/DownloadSettings/Warning/message.properties b/core/src/main/resources/jenkins/model/DownloadSettings/Warning/message.properties
new file mode 100644
index 0000000000000000000000000000000000000000..57c6a3b68dbf987ff13efbd627d9d6410ca5c9d3
--- /dev/null
+++ b/core/src/main/resources/jenkins/model/DownloadSettings/Warning/message.properties
@@ -0,0 +1,26 @@
+# The MIT License
+#
+# Copyright 2015 Jesse Glick.
+#
+# 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.
+
+blurb=\
+ You currently are using browser-based download to retrieve metadata for Jenkins plugins and tools. \
+ This has reliability issues and is not considered fully secure. \
+ Consider switching to server-based download.
diff --git a/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy b/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy
index 9c8987f561b0b4b1c7f41e7a53d7560ef546e110..6b0751e5634286586463211f2dd690d829b11321 100644
--- a/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy
+++ b/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy
@@ -2,8 +2,7 @@ package jenkins.security.DownloadSettings
def f = namespace(lib.FormTagLib)
-f.section(title: _("Download Preferences")) {
- f.entry(title: _("Use Browser"), field: "useBrowser") {
- f.checkbox()
- }
+// TODO avoid indentation somehow
+f.entry(field: "useBrowser") {
+ f.checkbox(title: _("Use browser for metadata download"))
}
diff --git a/core/src/main/resources/jenkins/model/DownloadSettings/help-useBrowser.html b/core/src/main/resources/jenkins/model/DownloadSettings/help-useBrowser.html
index ffe660600830b2cb755a51bb52bfbc5feb16a51c..1357dbaa056ec31c5a142c5b1fe4b395fd06d08c 100644
--- a/core/src/main/resources/jenkins/model/DownloadSettings/help-useBrowser.html
+++ b/core/src/main/resources/jenkins/model/DownloadSettings/help-useBrowser.html
@@ -1,7 +1,11 @@
-
+
Check to force the user’s browser to download metadata (lists of available plugins, tools etc.)
rather than Jenkins itself doing it.
Actual file downloads (plugins, tools) will still happen from Jenkins itself,
but this can be used to at least see new metadata when Jenkins cannot access the Internet
(but your own browser can, perhaps using some special proxy that Jenkins is not configured to use).
-
+
+
+ Use of browser mode is discouraged.
+ It has robustness problems and has been limited to users with Overall/Administer.
+
diff --git a/core/src/test/java/hudson/PluginManagerTest.java b/core/src/test/java/hudson/PluginManagerTest.java
index fe674b7d5a9c4e63434cdc0609fc2584769951c8..da8e6a5a8896c13ec255894ef04bde3d66d719c8 100644
--- a/core/src/test/java/hudson/PluginManagerTest.java
+++ b/core/src/test/java/hudson/PluginManagerTest.java
@@ -26,11 +26,14 @@ package hudson;
import org.apache.tools.ant.filters.StringInputStream;
import org.junit.Test;
-
-
+import org.xml.sax.SAXException;
+import java.io.IOException;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
+import org.jvnet.hudson.test.Issue;
public class PluginManagerTest {
@@ -41,4 +44,28 @@ public class PluginManagerTest {
.parseRequestedPlugins(new StringInputStream("")).toString());
}
+ @Issue("SECURITY-167")
+ @Test
+ public void parseInvalidRequestedPlugins() throws Exception {
+ String evilXML = "\n" +
+ "]>\n" +
+ "\n" +
+ " \n" +
+ "&foo;" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n";
+
+ PluginManager pluginManager = new LocalPluginManager(Util.createTempDir());
+ try {
+ pluginManager.parseRequestedPlugins(new StringInputStream(evilXML));
+ fail("XML contains an external entity, but no exception was thrown.");
+ }
+ catch (IOException ex) {
+ assertThat(ex.getCause(), instanceOf(SAXException.class));
+ assertThat(ex.getCause().getMessage(), containsString("Refusing to resolve entity with publicId(null) and systemId (file:///)"));
+ }
+ }
}
diff --git a/core/src/test/java/jenkins/util/VirtualFileTest.java b/core/src/test/java/jenkins/util/VirtualFileTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..46bfecd8d62213b54d70d2c06e9ada43140207fd
--- /dev/null
+++ b/core/src/test/java/jenkins/util/VirtualFileTest.java
@@ -0,0 +1,67 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2015 Jesse Glick.
+ *
+ * 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.util;
+
+import hudson.Util;
+import hudson.model.TaskListener;
+import java.io.File;
+import java.io.FileNotFoundException;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.jvnet.hudson.test.Issue;
+
+public class VirtualFileTest {
+
+ @Rule public TemporaryFolder tmp = new TemporaryFolder();
+
+ @Issue("SECURITY-162")
+ @Test public void outsideSymlinks() throws Exception {
+ File ws = tmp.newFolder("ws");
+ FileUtils.write(new File(ws, "safe"), "safe");
+ Util.createSymlink(ws, "safe", "supported", TaskListener.NULL);
+ File other = tmp.newFolder("other");
+ FileUtils.write(new File(other, "secret"), "s3cr3t");
+ Util.createSymlink(ws, "../other/secret", "hack", TaskListener.NULL);
+ VirtualFile root = VirtualFile.forFile(ws);
+ VirtualFile supported = root.child("supported");
+ assertTrue(supported.isFile());
+ assertTrue(supported.exists());
+ assertEquals("safe", IOUtils.toString(supported.open(), (String) null));
+ VirtualFile hack = root.child("hack");
+ assertFalse(hack.isFile());
+ assertFalse(hack.exists());
+ try {
+ hack.open();
+ fail();
+ } catch (FileNotFoundException x) {
+ // OK
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/jenkins/xml/XMLUtilsTest.java b/core/src/test/java/jenkins/xml/XMLUtilsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b71700ba0d12d56a72426e5c6c5a918756d7c5bd
--- /dev/null
+++ b/core/src/test/java/jenkins/xml/XMLUtilsTest.java
@@ -0,0 +1,100 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2015 James Nord.
+ *
+ * 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.xml;
+
+import jenkins.util.xml.XMLUtils;
+
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertThat;
+import org.jvnet.hudson.test.Issue;
+
+public class XMLUtilsTest {
+
+ @Issue("SECURITY-167")
+ @Test()
+ public void testSafeTransformDoesNotProcessForeignResources() throws Exception {
+ final String xml = "\n" +
+ "\n" +
+ "]>\n" +
+ "\n" +
+ " \n" +
+ " &foo;\n" +
+ " false\n" +
+ " \n" +
+ " \n" +
+ " true\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "";
+
+
+ StringWriter stringWriter = new StringWriter();
+ try {
+ XMLUtils.safeTransform(new StreamSource(new StringReader(xml)), new StreamResult(stringWriter));
+ // if no exception then JAXP is swallowing these - so there should be no entity in the description.
+ assertThat(stringWriter.toString(), containsString(""));
+ } catch (TransformerException ex) {
+ assertThat(ex.getMessage(), containsString("Refusing to resolve entity"));
+ }
+
+ }
+
+
+ @Issue("SECURITY-167")
+ @Test()
+ public void testUpdateByXmlIDoesNotFail() throws Exception {
+ final String xml = "\n" +
+ "\n" +
+ " \n" +
+ " &\n" +
+ " false\n" +
+ " \n" +
+ " \n" +
+ " true\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "";
+
+ StringWriter stringWriter = new StringWriter();
+
+ XMLUtils.safeTransform(new StreamSource(new StringReader(xml)), new StreamResult(stringWriter));
+ // make sure that normal entities are retained.
+ assertThat(stringWriter.toString(), containsString("&"));
+ }
+
+}
diff --git a/debian/debian/changelog b/debian/debian/changelog
index 18032a8e704ef72340685c83f215b458b4b93da0..0b3b4ad0e3b0944c5d67efcfdf63b2e1af39fc49 100644
--- a/debian/debian/changelog
+++ b/debian/debian/changelog
@@ -1,3 +1,9 @@
+jenkins (1.600) unstable; urgency=low
+
+ * See http://jenkins-ci.org/changelog for more details.
+
+ -- Kohsuke Kawaguchi Sat, 28 Feb 2015 09:47:38 -0800
+
jenkins (1.599) unstable; urgency=low
* See http://jenkins-ci.org/changelog for more details.
diff --git a/pom.xml b/pom.xml
index e384aee7f50d30fdc2311f1d91e1b386c0e771a5..b68188198ebb1ab8eebd31617fabfa62625e1a47 100644
--- a/pom.xml
+++ b/pom.xml
@@ -98,6 +98,7 @@ THE SOFTWARE.
1.7.7
2.7.1
+ 1.4.1
${skipTests}
6
diff --git a/test/pom.xml b/test/pom.xml
index a7ef9c7be4be55b0343c6095272215448af39b46..e31df5fb3f257b27977e2261b7d58db5283c864c 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -93,7 +93,7 @@ THE SOFTWARE.
org.jenkins-ci.plugins
matrix-project
- 1.0-beta-1
+ ${matrix-project.version}
org.jenkins-ci.plugins
diff --git a/test/src/test/java/hudson/model/AbstractItemSecurityTest.java b/test/src/test/java/hudson/model/AbstractItemSecurityTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..653097d29574cc796d1efb5237acf4ef9a358e2b
--- /dev/null
+++ b/test/src/test/java/hudson/model/AbstractItemSecurityTest.java
@@ -0,0 +1,87 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2015 James Nord.
+ *
+ * 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.model;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import javax.xml.transform.stream.StreamSource;
+import java.io.IOException;
+import java.io.StringReader;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.hamcrest.text.IsEmptyString.isEmptyOrNullString;
+import static org.junit.Assert.assertThat;
+import org.jvnet.hudson.test.Issue;
+
+public class AbstractItemSecurityTest {
+
+ @Rule
+ public JenkinsRule jenkinsRule = new JenkinsRule();
+
+ @Issue("SECURITY-167")
+ @Test()
+ public void testUpdateByXmlDoesNotProcessForeignResources() throws Exception {
+ final String xml = "\n" +
+ "\n" +
+ "]>\n" +
+ "\n" +
+ " &foo;\n" +
+ " \n" +
+ "";
+
+ FreeStyleProject project = jenkinsRule.createFreeStyleProject("security-167");
+ project.setDescription("Wibble");
+ try {
+ project.updateByXml(new StreamSource(new StringReader(xml)));
+ // if we didn't fail JAXP has thrown away the entity.
+ assertThat(project.getDescription(), isEmptyOrNullString());
+ } catch (IOException ex) {
+ assertThat(ex.getCause(), not(nullValue()));
+ assertThat(ex.getCause().getMessage(), containsString("Refusing to resolve entity"));
+ }
+
+ }
+
+
+ @Issue("SECURITY-167")
+ @Test()
+ public void testUpdateByXmlDoesNotFail() throws Exception {
+ final String xml = "\n" +
+ "\n" +
+ " &\n" +
+ " \n" +
+ "";
+
+ FreeStyleProject project = jenkinsRule.createFreeStyleProject("security-167");
+ project.updateByXml((StreamSource) new StreamSource(new StringReader(xml)));
+ assertThat(project.getDescription(), is("&")); // the entity is transformed
+ }
+
+}
diff --git a/test/src/test/java/hudson/model/ApiTest.java b/test/src/test/java/hudson/model/ApiTest.java
index bb2ea6f2f7bdb1c9d1abd02c832db63344f4f40a..737fd81474e5e7a8f0c8e704317359dc04c54574 100644
--- a/test/src/test/java/hudson/model/ApiTest.java
+++ b/test/src/test/java/hudson/model/ApiTest.java
@@ -23,9 +23,11 @@
*/
package hudson.model;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
import com.gargoylesoftware.htmlunit.Page;
+
+import java.io.File;
import java.net.HttpURLConnection;
import org.junit.Rule;
@@ -54,6 +56,27 @@ public class ApiTest {
assertEquals("", page.getWebResponse().getContentAsString());
}
+ /**
+ * Test that calling the XML API with the XPath document
function fails.
+ *
+ * @throws Exception if so
+ */
+ @Issue("SECURITY-165")
+ @Test public void xPathDocumentFunction() throws Exception {
+ File f = new File(j.jenkins.getRootDir(), "queue.xml");
+ JenkinsRule.WebClient client = j.createWebClient();
+
+ try {
+ client.goTo("api/xml?xpath=document(\"" + f.getAbsolutePath() + "\")", "application/xml");
+ fail("Should become 500 error");
+ } catch (com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException e) {
+ String contentAsString = e.getResponse().getContentAsString();
+ j.assertStringContains(
+ contentAsString,
+ "Illegal function: document");
+ }
+ }
+
@Test
@Issue("JENKINS-3267")
public void wrappedOneItem() throws Exception {
diff --git a/test/src/test/java/hudson/model/DownloadService2Test.java b/test/src/test/java/hudson/model/DownloadService2Test.java
new file mode 100644
index 0000000000000000000000000000000000000000..b388b131fa41784903b0849d00b7cb28c0728a5c
--- /dev/null
+++ b/test/src/test/java/hudson/model/DownloadService2Test.java
@@ -0,0 +1,65 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2015 Jesse Glick.
+ *
+ * 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.model;
+
+import hudson.util.FormValidation;
+import java.net.URL;
+import java.util.Set;
+import java.util.TreeSet;
+import net.sf.json.JSONObject;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.Rule;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.WithoutJenkins;
+
+@Issue("SECURITY-163")
+public class DownloadService2Test {
+
+ @Rule public JenkinsRule r = new JenkinsRule();
+
+ @Test public void updateNow() throws Exception {
+ for (DownloadService.Downloadable d : DownloadService.Downloadable.all()) {
+ FormValidation v = d.updateNow();
+ assertEquals(v.toString(), FormValidation.Kind.OK, v.kind);
+ }
+ }
+
+ @WithoutJenkins
+ @Test public void loadJSONHTML() throws Exception {
+ assertRoots("[list, signature]", "hudson.tasks.Maven.MavenInstaller.json.html"); // format used by most tools
+ assertRoots("[data, signature, version]", "hudson.tools.JDKInstaller.json.html"); // anomalous format
+ }
+
+ private static void assertRoots(String expected, String file) throws Exception {
+ URL resource = DownloadService2Test.class.getResource(file);
+ assertNotNull(file, resource);
+ JSONObject json = JSONObject.fromObject(DownloadService.loadJSONHTML(resource));
+ @SuppressWarnings("unchecked") Set keySet = json.keySet();
+ assertEquals(expected, new TreeSet(keySet).toString());
+ }
+
+}
diff --git a/test/src/test/java/hudson/model/DownloadServiceTest.java b/test/src/test/java/hudson/model/DownloadServiceTest.java
index 387e2ad4cbfdf10765bdb4aa297c263551234127..268b3f9fab10832945045983484ff5bce06d4794 100644
--- a/test/src/test/java/hudson/model/DownloadServiceTest.java
+++ b/test/src/test/java/hudson/model/DownloadServiceTest.java
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.net.URL;
import java.util.Set;
import java.util.TreeSet;
+import jenkins.model.DownloadSettings;
import net.sf.json.JSONObject;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.HudsonTestCase;
@@ -30,6 +31,7 @@ public class DownloadServiceTest extends HudsonTestCase {
// to bypass the URL restriction, we'll trigger downloadService.download ourselves
job = new Downloadable("test", "UNUSED");
Downloadable.all().add(job);
+ DownloadSettings.get().setUseBrowser(true);
}
@Issue("JENKINS-5536")
diff --git a/test/src/test/java/hudson/model/UpdateCenter2Test.java b/test/src/test/java/hudson/model/UpdateCenter2Test.java
index e3b8292280f1a9d0e9c073b5c814290a31d4764e..e6f41a6c1f65abc445acb4b294204e333f930ac4 100644
--- a/test/src/test/java/hudson/model/UpdateCenter2Test.java
+++ b/test/src/test/java/hudson/model/UpdateCenter2Test.java
@@ -46,7 +46,7 @@ public class UpdateCenter2Test {
@RandomlyFails("SocketTimeoutException from goTo due to GET http://localhost:…/update-center.json?…")
@Test public void install() throws Exception {
UpdateSite.neverUpdate = false;
- j.createWebClient().goTo(""); // load the metadata
+ j.jenkins.pluginManager.doCheckUpdatesServer(); // load the metadata
DownloadJob job = (DownloadJob) j.jenkins.getUpdateCenter().getPlugin("changelog-history").deploy().get(); // this seems like one of the smallest plugin
System.out.println(job.status);
assertTrue(job.status instanceof Success);
diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealm2Test.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealm2Test.java
new file mode 100644
index 0000000000000000000000000000000000000000..d42a475211f4e77b5baee00571011b3b5e449798
--- /dev/null
+++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealm2Test.java
@@ -0,0 +1,118 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2015, 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.security;
+
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import hudson.model.User;
+import hudson.security.pages.SignupPage;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.*;
+import org.jvnet.hudson.test.Issue;
+
+/**
+ * Tests the signup page of {@link hudson.security.HudsonPrivateSecurityRealm}
+ * TODO: Add to {@link hudson.security.HudsonPrivateSecurityRealmTest} going forward
+ */
+public class HudsonPrivateSecurityRealm2Test {
+
+
+ @Rule
+ public JenkinsRule rule = new JenkinsRule();
+
+ @Test
+ public void signup() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null);
+ rule.jenkins.setSecurityRealm(securityRealm);
+ JenkinsRule.WebClient wc = rule.createWebClient();
+ SignupPage signup = new SignupPage(wc.goTo("signup"));
+ signup.enterUsername("alice");
+ signup.enterPassword("alice");
+ signup.enterFullName("Alice User");
+ signup.enterEmail("alice@nowhere.com");
+ HtmlPage success = signup.submit(rule);
+ assertThat(success.getElementById("main-panel").getTextContent(), containsString("Success"));
+ assertThat(success.getAnchorByHref("/jenkins/user/alice").getTextContent(), containsString("Alice User"));
+
+
+ assertEquals("Alice User", securityRealm.getUser("alice").getDisplayName());
+
+ }
+
+ @Issue("SECURITY-166")
+ @Test
+ public void anonymousCantSignup() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null);
+ rule.jenkins.setSecurityRealm(securityRealm);
+ JenkinsRule.WebClient wc = rule.createWebClient();
+ SignupPage signup = new SignupPage(wc.goTo("signup"));
+ signup.enterUsername("anonymous");
+ signup.enterFullName("Bob");
+ signup.enterPassword("nothing");
+ signup.enterEmail("noone@nowhere.com");
+ signup = new SignupPage(signup.submit(rule));
+ signup.assertErrorContains("prohibited as a username");
+ assertNull(User.get("anonymous", false, Collections.emptyMap()));
+ }
+
+ @Issue("SECURITY-166")
+ @Test
+ public void systemCantSignup() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null);
+ rule.jenkins.setSecurityRealm(securityRealm);
+ JenkinsRule.WebClient wc = rule.createWebClient();
+ SignupPage signup = new SignupPage(wc.goTo("signup"));
+ signup.enterUsername("system");
+ signup.enterFullName("Bob");
+ signup.enterPassword("nothing");
+ signup.enterEmail("noone@nowhere.com");
+ signup = new SignupPage(signup.submit(rule));
+ signup.assertErrorContains("prohibited as a username");
+ assertNull(User.get("system",false, Collections.emptyMap()));
+ }
+
+ /**
+ * We don't allow prohibited fullnames since this may encumber auditing.
+ */
+ @Issue("SECURITY-166")
+ @Test
+ public void fullNameOfUnknownCantSignup() throws Exception {
+ HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null);
+ rule.jenkins.setSecurityRealm(securityRealm);
+ JenkinsRule.WebClient wc = rule.createWebClient();
+ SignupPage signup = new SignupPage(wc.goTo("signup"));
+ signup.enterUsername("unknown2");
+ signup.enterPassword("unknown2");
+ signup.enterFullName("unknown");
+ signup.enterEmail("noone@nowhere.com");
+ signup = new SignupPage(signup.submit(rule));
+ signup.assertErrorContains("prohibited as a full name");
+ assertNull(User.get("unknown2",false, Collections.emptyMap()));
+ }
+}
diff --git a/test/src/test/java/hudson/security/pages/SignupPage.java b/test/src/test/java/hudson/security/pages/SignupPage.java
new file mode 100644
index 0000000000000000000000000000000000000000..5007bce1b5cf062dc24877b036cf09861f3c427a
--- /dev/null
+++ b/test/src/test/java/hudson/security/pages/SignupPage.java
@@ -0,0 +1,65 @@
+package hudson.security.pages;
+
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.*;
+
+/**
+ * The the signup page for {@link hudson.security.HudsonPrivateSecurityRealm}
+ */
+public class SignupPage {
+
+ public HtmlForm signupForm;
+ public final HtmlPage signupPage;
+
+ public SignupPage(HtmlPage signupPage) {
+ this.signupPage = signupPage;
+
+ assertNotNull("The sign up page has a username field.", this.signupPage.getElementById("username"));
+ for (HtmlForm signupForm : this.signupPage.getForms()) {
+ if (signupForm.getInputsByName("username").size() == 0)
+ continue;
+ this.signupForm = signupForm;
+ }
+
+ }
+
+
+
+ public void enterUsername(String username) {
+ signupForm.getInputByName("username").setValueAttribute(username);
+ }
+
+ /**
+ * Enters the password in password1 and password2.
+ * You can then call {@link #enterPassword2(String)} if you want them to be different.
+ * @param password
+ */
+ public void enterPassword(String password) {
+ signupForm.getInputByName("password1").setValueAttribute(password);
+ signupForm.getInputByName("password2").setValueAttribute(password);
+ }
+
+ public void enterPassword2(String password2) {
+ signupForm.getInputByName("password2").setValueAttribute(password2);
+ }
+
+ public void enterFullName(String fullName) {
+ signupForm.getInputByName("fullname").setValueAttribute(fullName);
+ }
+
+ public void enterEmail(String email) {
+ signupForm.getInputByName("email").setValueAttribute(email);
+ }
+
+ public HtmlPage submit(JenkinsRule rule) throws Exception {
+ return rule.submit(signupForm);
+ }
+
+ public void assertErrorContains(String msg) {
+ assertThat(signupForm.getElementById("main-panel").getTextContent(),containsString(msg));
+ }
+}
diff --git a/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java b/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java
index c858bd130bee8a972d8db3d779f2e07145b6a1c4..77008ff121a4b839517d2d52ceb74334c1b26aa8 100644
--- a/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java
+++ b/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java
@@ -36,6 +36,7 @@ import hudson.model.Result;
import static hudson.tasks.LogRotatorTest.build;
import java.io.File;
import java.io.IOException;
+import java.net.HttpURLConnection;
import java.util.Collections;
import java.util.List;
import jenkins.util.VirtualFile;
@@ -126,6 +127,34 @@ public class ArtifactArchiverTest {
assertEquals("lodge", kids[0].getName());
// do not check that it .exists() since its target has not been archived
}
+
+ @Issue("SECURITY-162")
+ @Test public void outsideSymlinks() throws Exception {
+ final FreeStyleProject p = j.createFreeStyleProject();
+ p.getBuildersList().add(new TestBuilder() {
+ @Override public boolean perform(AbstractBuild,?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
+ FilePath ws = build.getWorkspace();
+ if (ws == null) {
+ return false;
+ }
+ ws.child("hack").symlinkTo(p.getConfigFile().getFile().getAbsolutePath(), listener);
+ return true;
+ }
+ });
+ p.getPublishersList().add(new ArtifactArchiver("hack", "", false, true));
+ FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0));
+ List artifacts = b.getArtifacts();
+ assertEquals(1, artifacts.size());
+ FreeStyleBuild.Artifact artifact = artifacts.get(0);
+ assertEquals("hack", artifact.relativePath);
+ VirtualFile[] kids = b.getArtifactManager().root().list();
+ assertEquals(1, kids.length);
+ assertEquals("hack", kids[0].getName());
+ assertFalse(kids[0].isDirectory());
+ assertFalse(kids[0].isFile());
+ assertFalse(kids[0].exists());
+ j.createWebClient().assertFails(b.getUrl() + "artifact/hack", HttpURLConnection.HTTP_NOT_FOUND);
+ }
private void runNewBuildAndStartUnitlIsCreated(AbstractProject project) throws InterruptedException{
int buildNumber = project.getNextBuildNumber();
diff --git a/test/src/test/resources/hudson/model/hudson.tasks.Maven.MavenInstaller.json.html b/test/src/test/resources/hudson/model/hudson.tasks.Maven.MavenInstaller.json.html
new file mode 100644
index 0000000000000000000000000000000000000000..18518c13e0d6af31246c1137318a1e7eed836a03
--- /dev/null
+++ b/test/src/test/resources/hudson/model/hudson.tasks.Maven.MavenInstaller.json.html
@@ -0,0 +1,158 @@
+
\ No newline at end of file
diff --git a/test/src/test/resources/hudson/model/hudson.tools.JDKInstaller.json.html b/test/src/test/resources/hudson/model/hudson.tools.JDKInstaller.json.html
new file mode 100644
index 0000000000000000000000000000000000000000..5b315032c5f5b12216d2bd423e2c4f50e765db7d
--- /dev/null
+++ b/test/src/test/resources/hudson/model/hudson.tools.JDKInstaller.json.html
@@ -0,0 +1,6498 @@
+
\ No newline at end of file
diff --git a/war/pom.xml b/war/pom.xml
index e34a0c7cd21dcdb2129b2e9ae68777bb450165c7..2face4ae5e3efd837f8a65e0661a0ad941951b3c 100644
--- a/war/pom.xml
+++ b/war/pom.xml
@@ -358,7 +358,13 @@ THE SOFTWARE.
org.jenkins-ci.plugins
matrix-project
- 1.3
+ ${matrix-project.version}
+ hpi
+
+
+ org.jenkins-ci.plugins
+ script-security
+ 1.13
hpi
diff --git a/war/src/main/webapp/images/100k.png b/war/src/main/webapp/images/100k.png
new file mode 100644
index 0000000000000000000000000000000000000000..11c1224e289ab3cc1f8b02f8cd229e1c2d29a14a
Binary files /dev/null and b/war/src/main/webapp/images/100k.png differ