Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions java/jenkins/xss/jelly-entry-unescaped-expression.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
rules:
- id: codevigilant.java.jenkins.xss.jelly-entry-unescaped-expression
message: >-
A Jelly form entry (f:entry/f:block) binds a ${...} expression to its
title/description while the view disables escaping via
`escapeEntryTitleAndDescription=false`. The expression is neither wrapped
in h.escape(...) nor the MarkupFormatter-sanitized
formattedDescription/getFormattedDescription getter, so any
configuration- or user-controlled content reaching the attribute is
emitted as raw HTML in the Jenkins UI (stored XSS, CWE-79). Escape the
value with h.escape(...) or render only the markup-formatter output.
severity: ERROR
languages:
- generic
paths:
include:
- "*.jelly"
- "*.xml"
patterns:
- pattern-inside: |
<j:set var="escapeEntryTitleAndDescription" value="false"/>
...
- pattern-regex: >-
(?is)<f:(?:entry|block)[^>]*\b(?:title|description)="\$\{(?!h\.escape\()(?![^"}]*getFormattedDescription\(\))(?![^"}]*\.formattedDescription\s*\})[^"]*"
metadata:
category: security
cwe: "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
owasp: "A03:2021 - Injection"
technology:
- jenkins
- jelly
confidence: MEDIUM
references:
- https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- https://www.jenkins.io/doc/developer/security/
source: ai-anant
license: MIT
17 changes: 17 additions & 0 deletions testcases/java/jelly-entry-unescaped-neg.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- negative repro: sanctioned shapes only (h.escape title + formatter-sanitized description, static literals, escaping left on) -->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<j:set var="escapeEntryTitleAndDescription" value="false"/>

<f:entry title="${h.escape(it.name)}" description="${it.formattedDescription}">
<f:textbox name="value" value="${it.value}"/>
</f:entry>

<f:entry title="${h.escape(it.name)}" description="${it.getFormattedDescription()}">
<f:textbox name="value" value="${it.value}"/>
</f:entry>

<f:entry title="Static title" description="Static description">
<f:checkbox name="value"/>
</f:entry>
</j:jelly>
20 changes: 20 additions & 0 deletions testcases/java/jelly-entry-unescaped-pos.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- positive repro: raw-HTML rendering of a custom description method while escaping is disabled -->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<j:set var="escapeEntryTitleAndDescription" value="false"/>

<j:if test="${it.isUserAdmin()}">
<f:entry title="${h.escape(it.name)}" description="${it.adminFormattedDescription()}">
<div name="parameter">
<input type="hidden" name="name" value="${it.name}"/>
<f:textbox name="value" value="${it.defaultValue4Build}"/>
</div>
</f:entry>
</j:if>

<j:if test="${it.isOnlyHidden() and !it.isUserAdmin()}">
<f:entry description="${it.adminFormattedDescription()}">
<f:checkbox title="${it.name}" name="value"/>
</f:entry>
</j:if>
</j:jelly>