<f:entry field="foo">
<f:textbox />
</f:entry>
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.
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.
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.
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();
}
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.
|