Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-60866] Add initial guidelines how to make code compatible with CSP #5301

Merged
merged 4 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions content/doc/developer/security/_chapter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ guides:
- rendering-user-content
- remoting-callables
- read-access
- csp
- misc
110 changes: 110 additions & 0 deletions content/doc/developer/security/csp.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
title: Content-Security-Policy Compatibility
layout: developerguide
---

== Introduction

From https://content-security-policy.com/[content-security-policy.com]:

> _Content-Security-Policy_ is the name of a HTTP response header that modern browsers use to enhance the security of the document (or web page). The Content-Security-Policy header allows you to restrict how resources such as JavaScript, CSS, or pretty much anything that the browser loads.

Using Content-Security-Policy (CSP), injection attacks like cross-site scripting can be prevented.

// TODO Keep up to date with versions
Unfortunately, as of Jenkins 2.360, the Jenkins classic UI is not yet compatible with the CSP directives that would allow preventing such injection attacks.

This guide documents how to write UI code in a manner that is expected to be compatible with future use of CSP directives on the Jenkins UI.



== Guidelines


=== Inline JavaScript blocks

Do not use inline JavaScript (JS) on the Jenkins UI, i.e., JS embedded in HTML output.
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved

This is typically done with `<script>` tags, like so:

[source, html]
<script type="text/javascript">
alert("Hello, world!");
</script>

The guidelines in link:/doc/developer/security/xss-prevention/#passing-values-to-javascript[the documentation on XSS prevention] can be useful to pass arguments to JavaScript, or otherwise control its behavior dynamically.

You can generally use https://github.com/jenkinsci/stapler/blob/master/docs/jelly-taglib-ref.adoc#adjunct[Stapler adjuncts] to load files related to UI views and ensure they are loaded only once.

Exmples of this are: https://github.com/jenkinsci/jenkins/pull/6849[jenkinsci/jenkins#6849]
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved


=== Inline event handlers

Event handlers like `onclick` or `onblur` would be defined in separate files.
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved

For this to work, the element that would have the inline event handler attribute(s) would need a class or ID by which it can be looked up from JS.
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved

Depending on how that element is added to the UI, you'd use on the the following methods to add event handlers:
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved

You can use `document.addEventListener('DOMContentLoaded', …)` when it's one or more elements that are present on the page from the moment it is loaded.
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved
Look up the elements by their ID or class or similar characteristics, then call `#addEventListener` on them.
Be mindful of Jenkins's extensibility, so consider including plugin names in element class names or IDs to prevent unintentional conflicts with other plugins.

Use `Behaviour#specify` to add event handlers to elements that may be dynamically added to the page, for example as part of AJAX responses.
One common instance of this is in configuration forms: `renderOnDemand` is used by common form elements like `hetero-list` to load parts of the page only as the form is being changed.
The code that adds content from AJAX responses dynamically to the page needs to call `Behaviour#applySubtree` on the newly added content.

Examples of this are: https://github.com/jenkinsci/jenkins/pull/5514[jenkinsci/jenkins#5514]

=== Legacy JavaScript `checkUrl` validation

Form validation using manually specified `checkUrl` parameters currently supports inline JS for its "legacy" mode.
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved
It looks like the following:

[source, html]
<f:textbox checkUrl="'${rootURL}/${h.jsStringEscape(it.url)}checkText?value='+encodeURIComponent(this.value)+'" … />

This combines inline JS and building parts of the string using JEXL expressions in Jelly, with different ways to escape different parts of the content to prevent injection vulnerabilities.

Instead, use the _modern_ `checkUrl` mode, which as of Jenkins 2.360 requires the `checkDependsOn` attribute to be set (but it can be an empty string).
This mode will automatically add the current form elements value as the query parameter called `value`, so the above examples can be simplified to the following:

[source, html]
<f:textbox checkUrl="${rootURL}/${it.url}checkText" … />
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved

Examples of this are: https://github.com/jenkinsci/jenkins/pull/6856[jenkinsci/jenkins#6856] https://github.com/jenkinsci/jenkins/pull/6857[jenkinsci/jenkins#6857]

To pass additional values, specify the respective form field names as part of the `checkDependsOn` string.

If you need to pass parameters that are not represented as form fields, the following options exist as of Jenkins 2.360:

* Define a new form validation endpoint.
This can be a viable option when it's a boolean value (2 endpoints instead of one).
* Define a hidden form field (wrap it in `f:invisibleEntry`) with the expected `name` and `value` and specify it in `checkDependsOn`.
Make sure to ignore it otherwise.
See https://github.com/jenkinsci/jenkins/pull/6859[jenkinsci/jenkins#6859] for an example.


=== `eval` calls

`eval` should not be used to interpret a string as JS code.

Depending on your use case, different solutions are possible.

To parse JSON, use `JSON.parse` instead.
See https://github.com/jenkinsci/jenkins/pull/6868[jenkinsci/jenkins#6868] for an example.

To invoke a callback, have the caller define a global function and pass its name as an argument.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to define globals seems awkward. Do you have an example of this case?

Copy link
Contributor Author

@daniel-beck daniel-beck Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var foo = function() { … } (or let foo = function() { … }) outside any block makes it global scoped (or, since it's functions, function foobar() { … }).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just meant that the general scenario of when you would need to do this is not really clear to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the part of jenkinsci/jenkins#6865 related to renderOnDemand.jelly.

Then your code can invoke like this:
daniel-beck marked this conversation as resolved.
Show resolved Hide resolved

[source, javascript]
/* someone else provides this */
let callbackName = 'foo';
/* invoke it with arguments */
window[callbackName](args);

== Testing

To test your code, you can use https://plugins.jenkins.io/csp/[Content Security Policy Plugin].
It implements CSP directives for the classic Jenkins UI.