Securely implementing form validation

This guide looks at form validation from a security point of view: What are the considerations for securing form validation, and how can they be best implemented?

First, let’s look at a typical form field defined in Jelly, and its validation method in the object’s descriptor. This forms the basis for the rest of this guide.

<f:entry field="foo">
  <f:textbox />
</f:entry>
public FormValidation doCheckFoo(@QueryParameter String value) {
  if (Util.fixEmptyAndTrim(value) == null) {
    return FormValidation.error("foo cannot be empty");
  }
  return FormValidation.ok();
}

Stapler routing and Jenkins behavior result in this method being routed at a URL like /descriptorByName/f.q.ClassName/checkFoo. This URL contains a minimal HTML snippet with a form validation message (if present) based on the presence and value of the value query parameter.

Potential Security Issues

Returning Private Information

By default, any user with Overall/Read access can access form validation methods like the example method above. Some form validation methods could be used to infer private information, and as such may need to be protected from unauthorized access.

If a form validation method is supposed to only be accessible to users with specific permissions, a permission check should be added.

Side Effects

Form validation can become quite complex and have side effects. For example, Jenkins might evaluate a Groovy script passed as parameter, or access a URL passed as parameter. These behaviors could be exploited to result in arbitrary code execution and server-side request forgery, respectively.

Ensuring that form validation code such as this is protected with appropriate permission checks, it might not be enough: Cross-site request forgery is an attack on users with the necessary privileges and exploits the trust Jenkins has in these legitimate users' web browsers. Protecting from these attacks requires that form validation methods with side effects only accept POST requests.

Implementing Form Validation

Checking permissions

To check for global permissions like Administer, add a call to Jenkins#checkPermission. Note that Overall/Read permission is always implied (unless you specifically implement UnprotectedRootAction).

public FormValidation doCheckFoo(@QueryParameter String value) {
  Jenkins.get().checkPermission(Jenkins.ADMINISTER); // or Jenkins.getInstance() on older core baselines
  if (Util.fixEmptyAndTrim(value) == null) {
    return FormValidation.error("foo cannot be empty");
  }
  return FormValidation.ok();
}

If a different AccessControlled's permissions are to be checked, as the form validation depends on the context in which the method is invoked (e.g. when writing a build step and validation depends on the identity of the item), you may need to add an @AncestorInPath annotated parameter to get to the necessary context.

public FormValidation doCheckFoo(@QueryParameter String value, @AncestorInPath Item item) {
  if (item == null) { // no context
    return FormValidation.ok();
  }
  item.checkPermission(Item.CONFIGURE);

  if (Util.fixEmptyAndTrim(value) == null) {
    return FormValidation.error("foo cannot be empty");
  }
  return FormValidation.ok();
}

Protecting from CSRF

Stapler web methods, like form validation, can be invoked using any HTTP verb by default. This can be used by malicious users to attack an application by way of its legitimate users: Cross-site request forgery is the result of a web application (Jenkins) trusting a legitimate user’s browser.

To prevent this, Jenkins includes CSRF protection that requires actions with side effects (POST requests) to submit a user-specific secret, called a crumb. To ensure that this protection is relevant, web methods with side effects need to reject requests not sent via POST. There are two options to achieve this in general:

  • POST limits processing to just the POST verb, and all other verbs receive a 404 response. This is the recommended approach for form validation methods.

  • RequirePOST is the older (and more common) approach. It shows a form that allows users to resubmit the request using POST if they used a different verb. This is mostly useful for simple API actions.

Applying this protection is as simple as adding the annotation to the method:

@POST
public FormValidation doCheckFoo(@QueryParameter String value) {
  if (Util.fixEmptyAndTrim(value) == null) {
    return FormValidation.error("foo cannot be empty");
  }
  return FormValidation.ok();
}

Depending on the versions of Jenkins supported by your plugin, however, these form validation methods may be invoked using HTTP GET, so the form may need to be adapted as well. The Jenkins form Jelly controls support the checkMethod attribute, which, if set to post, results in form validation being invoked via HTTP POST:

<f:entry field="foo">
  <f:textbox checkMethod="post" />
</f:entry>

The default behavior of form controls has changed over time, so you may not need to add the checkMethod attribute, depending on the versions of Jenkins supported by your plugin and the types of form controls that have validation:

  • Historically, form validation requests were sent using GET by default (with the exception of f:validateButton, which always used POST).

  • Since Jenkins 2.84 and 2.73.3, f:password always sends form validation as POST.

  • Since Jenkins 2.285, most other form controls send form validation requests as POST by default, and checkMethod only opts out.

  • As of Jenkins 2.300 f:checkbox does not support the checkMethod attribute at all and requests are always sent using GET.

Some basic form controls may not declare the checkMethod attribute in older versions of Jenkins. Depending on the control, it may still work, despite an error shown in your IDE.