/** * Copyright (c) 2008-2009 Yahoo! Inc. * All rights reserved. * The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php) */ package hudson.security.csrf; import javax.servlet.ServletRequest; import hudson.init.Initializer; import jenkins.model.Jenkins; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.WebApp; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import hudson.DescriptorExtensionList; import hudson.ExtensionPoint; import hudson.model.Api; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.util.MultipartFormDataParser; import java.io.IOException; import java.io.OutputStream; import javax.servlet.ServletException; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerResponse; /** * A CrumbIssuer represents an algorithm to generate a nonce value, known as a * crumb, to counter cross site request forgery exploits. Crumbs are typically * hashes incorporating information that uniquely identifies an agent that sends * a request, along with a guarded secret so that the crumb value cannot be * forged by a third party. * * @author dty * @see <a href="http://en.wikipedia.org/wiki/XSRF">Wikipedia: Cross site request forgery</a> */ @ExportedBean public abstract class CrumbIssuer implements Describable<CrumbIssuer>, ExtensionPoint { private static final String CRUMB_ATTRIBUTE = CrumbIssuer.class.getName() + "_crumb"; @Restricted(NoExternalUse.class) public static final String DEFAULT_CRUMB_NAME = "Jenkins-Crumb"; /** * Get the name of the request parameter the crumb will be stored in. Exposed * here for the remote API. */ @Exported public String getCrumbRequestField() { return getDescriptor().getCrumbRequestField(); } /** * Get a crumb value based on user specific information in the current request. * Intended for use only by the remote API. */ @Exported public String getCrumb() { return getCrumb(Stapler.getCurrentRequest()); } /** * Get a crumb value based on user specific information in the request. * @param request */ public String getCrumb(ServletRequest request) { String crumb = null; if (request != null) { crumb = (String) request.getAttribute(CRUMB_ATTRIBUTE); } if (crumb == null) { crumb = issueCrumb(request, getDescriptor().getCrumbSalt()); if (request != null) { if ((crumb != null) && crumb.length()>0) { request.setAttribute(CRUMB_ATTRIBUTE, crumb); } else { request.removeAttribute(CRUMB_ATTRIBUTE); } } } return crumb; } /** * Create a crumb value based on user specific information in the request. * The crumb should be generated by building a cryptographic hash of: * <ul> * <li>relevant information in the request that can uniquely identify the client * <li>the salt value * <li>an implementation specific guarded secret. * </ul> * * @param request * @param salt */ protected abstract String issueCrumb(ServletRequest request, String salt); /** * Get a crumb from a request parameter and validate it against other data * in the current request. The salt and request parameter that is used is * defined by the current configuration. * * @param request */ public boolean validateCrumb(ServletRequest request) { CrumbIssuerDescriptor<CrumbIssuer> desc = getDescriptor(); String crumbField = desc.getCrumbRequestField(); String crumbSalt = desc.getCrumbSalt(); return validateCrumb(request, crumbSalt, request.getParameter(crumbField)); } /** * Get a crumb from multipart form data and validate it against other data * in the current request. The salt and request parameter that is used is * defined by the current configuration. * * @param request * @param parser */ public boolean validateCrumb(ServletRequest request, MultipartFormDataParser parser) { CrumbIssuerDescriptor<CrumbIssuer> desc = getDescriptor(); String crumbField = desc.getCrumbRequestField(); String crumbSalt = desc.getCrumbSalt(); return validateCrumb(request, crumbSalt, parser.get(crumbField)); } /** * Validate a previously created crumb against information in the current request. * * @param request * @param salt * @param crumb The previously generated crumb to validate against information in the current request */ public abstract boolean validateCrumb(ServletRequest request, String salt, String crumb); /** * Access global configuration for the crumb issuer. */ public CrumbIssuerDescriptor<CrumbIssuer> getDescriptor() { return (CrumbIssuerDescriptor<CrumbIssuer>) Jenkins.getInstance().getDescriptorOrDie(getClass()); } /** * Returns all the registered {@link CrumbIssuer} descriptors. */ public static DescriptorExtensionList<CrumbIssuer, Descriptor<CrumbIssuer>> all() { return Jenkins.getInstance().<CrumbIssuer, Descriptor<CrumbIssuer>>getDescriptorList(CrumbIssuer.class); } public Api getApi() { return new RestrictedApi(this); } /** * Sets up Stapler to use our crumb issuer. */ @Initializer public static void initStaplerCrumbIssuer() { WebApp.get(Jenkins.getInstance().servletContext).setCrumbIssuer(new org.kohsuke.stapler.CrumbIssuer() { @Override public String issueCrumb(StaplerRequest request) { CrumbIssuer ci = Jenkins.getInstance().getCrumbIssuer(); return ci!=null ? ci.getCrumb(request) : DEFAULT.issueCrumb(request); } @Override public void validateCrumb(StaplerRequest request, String submittedCrumb) { CrumbIssuer ci = Jenkins.getInstance().getCrumbIssuer(); if (ci==null) { DEFAULT.validateCrumb(request,submittedCrumb); } else { if (!ci.validateCrumb(request, ci.getDescriptor().getCrumbSalt(), submittedCrumb)) throw new SecurityException("Crumb didn't match"); } } }); } @Restricted(NoExternalUse.class) public static class RestrictedApi extends Api { RestrictedApi(CrumbIssuer instance) { super(instance); } @Override public void doXml(StaplerRequest req, StaplerResponse rsp, @QueryParameter String xpath, @QueryParameter String wrapper, @QueryParameter String tree, @QueryParameter int depth) throws IOException, ServletException { String text; CrumbIssuer ci = (CrumbIssuer) bean; if ("/*/crumbRequestField/text()".equals(xpath)) { // old FullDuplexHttpStream text = ci.getCrumbRequestField(); } else if ("/*/crumb/text()".equals(xpath)) { // ditto text = ci.getCrumb(); } else if ("concat(//crumbRequestField,\":\",//crumb)".equals(xpath)) { // new FullDuplexHttpStream; Main text = ci.getCrumbRequestField() + ':' + ci.getCrumb(); } else if ("concat(//crumbRequestField,'=',//crumb)".equals(xpath)) { // NetBeans if (ci.getCrumbRequestField().startsWith(".") || ci.getCrumbRequestField().contains("-")) { text = ci.getCrumbRequestField() + '=' + ci.getCrumb(); } else { text = null; } } else { text = null; } if (text != null) { OutputStream o = rsp.getCompressedOutputStream(req); try { rsp.setContentType("text/plain;charset=UTF-8"); o.write(text.getBytes("UTF-8")); } finally { o.close(); } } else { super.doXml(req, rsp, xpath, wrapper, tree, depth); } } } }