Skip to content

Dropdown/jumplist menu items with multi-parameter query string hrefs get corrupted by double XML-escaping #27202

Description

@yakovakmurzin

Jenkins and plugins versions report

Jenkins: 2.568.1
OS: Linux - 6.1.177-224.371.amzn2023.x86_64
Java: 21.0.11 - Eclipse Adoptium (OpenJDK 64-Bit Server VM)
---
amazon-ecr:1.161.v1a_1e8df852d6
amazon-inspector-image-scanner:634.v9347247dc855
analysis-model-api:14.14.0-1004.vf53c9efb_f455
ansicolor:536.v13fa_b_860c267
ant:520.vd082ecfb_16a_9
antisamy-markup-formatter:173.v680e3a_b_69ff3
apache-httpcomponents-client-4-api:4.5.14-269.vfa_2321039a_83
apache-httpcomponents-client-5-api:5.6.2-199.vc3c04d033fcc
artifact-manager-s3:986.v7c9a_d15576b_b_
artifactory:4.0.8
asm-api:9.10.1-216.va_9256d3b_844b_
audit-trail:455.v2ee2f7a_da_b_25
authentication-tokens:1.144.v5ff4a_5ec5c33
aws-credentials:265.v8422a_f384cd9
aws-global-configuration:165.v712c4a_e4a_078
aws-java-sdk:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-api-gateway:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-autoscaling:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-cloudformation:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-cloudfront:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-cloudwatch:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-codebuild:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-codedeploy:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-ec2:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-ecr:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-ecs:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-efs:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-elasticbeanstalk:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-elasticloadbalancingv2:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-iam:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-kinesis:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-lambda:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-logs:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-minimal:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-organizations:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-secretsmanager:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-sns:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-sqs:1.12.780-480.v4a_0819121a_9e
aws-java-sdk-ssm:1.12.780-480.v4a_0819121a_9e
aws-java-sdk2-cloudwatch:2.42.33-70.vd69c0763fa_60
aws-java-sdk2-core:2.42.33-70.vd69c0763fa_60
aws-java-sdk2-ec2:2.42.33-70.vd69c0763fa_60
aws-java-sdk2-ecr:2.42.33-70.vd69c0763fa_60
aws-java-sdk2-s3:2.42.33-70.vd69c0763fa_60
aws-java-sdk2-ssm:2.42.33-70.vd69c0763fa_60
badge:3.605.v7a_3387b_97a_b_6
basic-branch-build-strategies:317.v85b_331d6cc42
blueocean:1.27.25
blueocean-bitbucket-pipeline:1.27.25
blueocean-commons:1.27.25
blueocean-config:1.27.25
blueocean-core-js:1.27.25
blueocean-dashboard:1.27.25
blueocean-display-url:2.4.4
blueocean-events:1.27.25
blueocean-git-pipeline:1.27.25
blueocean-github-pipeline:1.27.25
blueocean-i18n:1.27.25
blueocean-jira:1.27.25
blueocean-jwt:1.27.25
blueocean-personalization:1.27.25
blueocean-pipeline-api-impl:1.27.25
blueocean-pipeline-editor:1.27.25
blueocean-pipeline-scm-api:1.27.25
blueocean-rest:1.27.25
blueocean-rest-impl:1.27.25
blueocean-web:1.27.25
bootstrap5-api:5.3.8-1048.va_c299057e35c
bouncycastle-api:2.30.1.84-291.v9f17b_21896e2
branch-api:2.1280.v0d4e5b_b_460ef
build-failure-analyzer:3.887.vc872cf1b_7dff
build-token-root:365.v717f8685a_09e
build-user-vars-plugin:214.va_eed2ed849ca_
build-with-parameters:81.ve4a_9c2499d9a
buildtriggerbadge:343.vb_693d3ceda_44
caffeine-api:3.2.4-208.v7e2da_a_7db_82b_
checks-api:415.vf022234a_931d
cloudbees-bitbucket-branch-source:937.3.6
cloudbees-folder:6.1100.ve9eed61d16c4
clover:5.1.0.711.v4a_ec136b_838a_
command-launcher:134.v025a_5fcf9dea_
commons-collections4-api:4.5.0-8.va_d5448ef9011
commons-compress-api:1.28.0-86.v905b_77d84797
commons-lang3-api:3.20.0-109.ve43756e2d2b_4
commons-text-api:1.15.0-218.va_61573470393
config-file-provider:1013.v73c323e52b_1f
configuration-as-code:2111.v475308a_6c93b_
content-replace:1.8.2
copyartifact:795.ve8e151429b_27
coverage:3.3311.v42c5a_f929e3e
credentials:1506.v948b_b_b_7dec44
credentials-binding:728.v902a_273b_8947
customizable-header:295.v2544b_ca_19b_97
dark-theme:652.vea_da_dfea_e769
data-tables-api:2.3.8-1570.v1cb_1cd2a_0fb_c
deploy-dashboard:0.1.0
display-url-api:2.217.va_6b_de84cc74b_
docker-commons:477.v289085a_b_6896
docker-workflow:647.vf474049b_b_303
durable-task:686.v80ff80875b_82
ec2:2059.v05f9d511d7e5
echarts-api:6.1.0-1306.vcee1648c16a_4
eddsa-api:0.3.0.1-29.v67e9a_1c969b_b_
emoji-symbols-api:17.0-57.v8d44b_9a_b_d5ea_
envinject:2.941.v351a_20c0a_3ca_
envinject-api:1.241.vdd714803b_403
extended-read-permission:68.vd270568a_7520
extensible-choice-parameter:255.vfa_41f46828ef
external-monitor-job:223.vb_fddcf42c9b_3
favorite:2.267.vb_90d08408081
file-operations:439.vdc9d3b_d74b_d9
flatpickr-api:4.6.13-32.v60a_51029c136
flock:1.0.1
folder-properties:62.v1636b_4a_84608
font-awesome-api:7.3.1-1013.v0835a_879ec6d
forensics-api:4.1891.v5e60f3377506
generic-tool:1.17.vb_8a_b_4e5600c9
generic-webhook-trigger:2.4.2
git:5.10.1
git-client:6.6.1
git-parameter:462.463.v496a_59f698e5
git-server:137.ve0060b_432302
github:1.47.0
github-api:1.330-492.v3941a_032db_2a_
github-branch-source:1967.1970.vd86979736546
gradle:2.19.1244.v1f9866817fec
groovy:537.v741a_5a_f1b_581
gson-api:2.14.0-201.v8eefe5515533
handy-uri-templates-2-api:2.1.8-38.vcea_5d521d5f3
hashicorp-vault-plugin:383.v7529eea_ef531
htmlpublisher:427.1
http_request:1.25
ignore-committer-strategy:57.v0756db_b_f6926
instance-identity:203.v15e81a_1b_7a_38
ionicons-api:94.vcc3065403257
jackson-annotations2-api:2.22-19.v10a_a_582ea_26e
jackson2-api:2.22.1-443.vc91f592333c4
jackson3-api:3.2.1-92.vd25c2e23c180
jakarta-activation-api:2.1.4-1
jakarta-mail-api:2.1.5-1
jakarta-xml-bind-api:4.0.9-19.v2b_a_5b_44d9a_1c
javadoc:354.vee1a_660b_4990
javax-activation-api:1.2.0-8
javax-mail-api:1.6.2-11
jaxb:2.3.9-143.v5979df3304e6
jdk-tool:83.v417146707a_3d
jenkins-design-language:1.27.25
jersey2-api:2.48-180.ve47b_264f849b_
jira:3.21
jjwt-api:0.13.0-141.vd58b_a_9592b_6c
job-dsl:3654.vdf58f53e2d15
job-restrictions:242.v6edda_c9e4ca_f
jobConfigHistory:1367.vc8fa_b_15101dc
joda-time-api:2.14.3-200.v65623733c99f
jquery:1.12.4-3
jquery3-api:3.7.1-687.v68d468e40b_30
jsch:0.2.16-95.v3eecb_55fa_b_78
json-api:20260719-223.va_81f828cdb_58
json-path-api:3.0.0-218.vcd4dd1355de2
jsoup:1.23.1-103.v4fde9422cc6f
junit:1416.vd753e036de5e
kubernetes:4540.v612369217f87
kubernetes-client-api:7.3.1-256.v788a_0b_787114
kubernetes-credentials:207.v492f58828b_ed
ldap:807.809.vd3a_4e5e4ec98
list-git-branches-parameter:0.0.13
lockable-resources:1539.v4db_b_fc1cc115
log-parser:3.0.4
mailer:534.v1b_36f5864073
mapdb-api:1.0.9-44.va_1e1310c9118
mask-passwords:220.v95819055b_265
matrix-auth:3.3
matrix-project:905.vcc6831e8760a_
maven-plugin:3.27
mercurial:1323.ve69d2a_db_8a_b_d
metrics:4.2.37-494.v06f9a_939d33a_
mina-sshd-api-common:2.17.1-187.v0341274c2905
mina-sshd-api-core:2.17.1-187.v0341274c2905
mina-sshd-api-scp:2.17.1-187.v0341274c2905
monitoring:2.8.0
mstest:1.0.5
next-build-number:66.v4b_4762172d53
node-iterator-api:72.vc90e81737df1
nunit:648.vdc8c6431f464
oidc-provider:219.v598fcf17d4b_c
okhttp-api:5.3.2-200.vedb_720a_cf1f8
oss-symbols-api:465.v25d9fdc88c26
pam-auth:1.12
parameterized-scheduler:379.v95c73f233a_df
performance:1015.v09ca_52b_3370e
pipeline-aws:1.45
pipeline-build-step:599.v4b_67ea_11b_152
pipeline-graph-analysis:254.v0f63a_a_447dca_
pipeline-graph-view:980.vb_db_0b_e5f683c
pipeline-groovy-lib:797.v90ea_a_9b_e45a_0
pipeline-input-step:560.v56198a_642157
pipeline-milestone-step:152.v6e22b_8cfc66c
pipeline-model-api:2.2291.v2934911987b_6
pipeline-model-definition:2.2291.v2934911987b_6
pipeline-model-extensions:2.2291.v2934911987b_6
pipeline-rest-api:2.41
pipeline-stage-step:345.va_96187909426
pipeline-stage-tags-metadata:2.2291.v2934911987b_6
pipeline-stage-view:2.41
pipeline-utility-steps:3.810.va_7672d206740
plain-credentials:199.v9f8e1f741799
plugin-usage-plugin:418.v308c3c863d25
plugin-util-api:7.1341.v039f146993d9
powershell:185.v7a_026da_c54ee
prism-api:1.30.0-741.v034eb_0b_0a_a_fa_
pubsub-light:1.19
rebuild:338.va_0a_b_50e29397
saml:4.618.v441a_27fa_46d2
scm-api:728.vc30dcf7a_0df5
script-security:1402.1405.vc96e74964250
simple-theme-plugin:230.v8b_fd91b_b_800c
snakeyaml-api:2.5-149.v72471e9c6371
snakeyaml-engine-api:3.0.1-5.vd98ea_ff3b_92e
sse-gateway:1.29
ssh-agent:405.v67cc4f9764d0
ssh-credentials:372.va_250881b_08cd
ssh-slaves:3.1097.v868116049892
ssh-steps:2.0.92.vb_a_0583935f9b_
sshd:3.384.vc89b_5e138cf9
stashNotifier:1.555.v8937f80894e4
structs:362.va_b_695ef4fdf9
templating-engine:2.5.5
theme-manager:346.v06cca_64c6a_37
throttle-concurrents:625.vc8b_e469e9a_b_c
timestamper:1.30
token-macro:477.vd4f0dc3cb_cf1
trilead-api:2.284.v1974ea_324382
uno-choice:2.8.9
validating-string-parameter:345.v0f4330488a_f6
variant:70.va_d9f17f859e0
vsphere-cloud:4.554.va_e21a_3cd25ea_
warnings-ng:13.10153.v011f47a_3ef01
woodstox-core-api:7.2.1-6.v3718a_a_11f5c4
workflow-aggregator:608.v67378e9d3db_1
workflow-api:1413.v2ff1a_5e720fa_
workflow-basic-steps:1098.v808b_fd7f8cf4
workflow-cps:4360.v2020e8819a_d6
workflow-durable-task-step:1479.v56e587f413a_7
workflow-job:1590.v49101d088542
workflow-multibranch:841.vec5b_9e1806ec
workflow-scm-step:466.va_d69e602552b_
workflow-step-api:724.v538c2362b_dfb_
workflow-support:1015.v785e5a_b_b_8b_22

What Operating System are you using (both controller, and any agents involved in the problem)?

Jenkins and plugins versions report

Jenkins: 2.568.1
Relevant plugin: deploy-dashboard 0.1.0 (uses buildAddUrl step to add an Action with a raw getUrlName() URL containing multiple &-separated query parameters), but this is a Jenkins core bug, not plugin-specific.

What Operating System are you using (both controller, and any agents involved in the problem)?

Controller: official Jenkins Docker image (jenkins/jenkins) running on an Amazon Linux host (AWS EC2).
This is a client-side (browser) rendering bug — reproducible independent of controller/agent OS, since the double-escaping happens in browser-side JS (templates.js) when rendering the dropdown/jumplist menu.

Reproduction steps

Reproduction steps

Setup (clean Jenkins, matches our production version):

  1. docker run -p 8080:8080 -p 50000:50000 jenkins/jenkins:2.568.1-lts
  2. Complete the setup wizard, install suggested plugins.
  3. Manage Jenkins → Plugins → Available plugins → install "Deploy Dashboard Plugin by Namecheap" (deploy-dashboard, https://plugins.jenkins.io/deploy-dashboard/) — this is the plugin providing the buildAddUrl step, but the bug reproduces with any Action whose getUrlName() returns a URL with multiple &-joined query parameters; buildAddUrl is just a convenient way to add one.
  4. Create a Freestyle (or Pipeline) job named target-job with 3 String Parameters: a, b, c. Give it a single "Execute shell" step: echo "a=$a b=$b c=$c".
  5. Create a second job named source-job (Pipeline) with this script:
   node {
       stage('Build') {
           buildAddUrl(
               title: 'Deploy to target-job',
               url: "/job/target-job/buildWithParameters?a=1&b=2&c=3"
           )
       }
   }
  1. Run source-job once (build Translation to Brasilian Portuguese Br #1).

Reproduce the bug (dropdown / jumplist):

  1. Go to source-job's main page (or any dashboard view listing it) — in the build history list on the left, hover/click the chevron (⌄) next to build Translation to Brasilian Portuguese Br #1 to open the Actions dropdown ("jumplist").
  2. Click "Deploy to target-job" in that dropdown.
  3. Open target-job → last build → check the parameter values. Actual: only a=1 is set; b and c are empty/default (they arrive as amp;b/amp;c, which target-job doesn't recognize as parameters).
  4. For direct proof without triggering a build: right-click the same dropdown link → Inspect, or in DevTools console run:
    document.querySelector('a[href*="target-job"]').getAttribute('href')
**Actual href:** `/job/target-job/buildWithParameters?a=1&b=2&c=3` (literal `&` text, not decoded, not a real `&`).

Compare with the working case (classic sidebar):

  1. Open build Translation to Brasilian Portuguese Br #1's own page (source-job#1). In the left sidebar, the same "Deploy to target-job" action link appears.
  2. Click it → target-job triggers correctly with a=1&b=2&c=3 all set.
  3. Inspect this sidebar link's href the same way — it correctly shows a literal &, not &.

Steps 9-10 vs 12-13 isolate the bug to the dropdown/jumplist JS renderer specifically; the server-rendered sidebar is unaffected.

Expected Results

When clicking an Action link (e.g. one added via buildAddUrl) from the build history dropdown/jumplist menu, all query parameters in the URL should be passed through to the target job unmodified — identical to clicking the same-titled link from the classic build sidebar.

Specifically, for a URL like /job/target-job/buildWithParameters?a=1&b=2&c=3, the resulting href in the DOM should contain a literal & between parameters (or its single, correctly-decoded HTML entity &), so that target-job receives all three parameters (a=1, b=2, c=3).

Actual Results

Only the first query parameter in the URL is passed to the target job. All subsequent parameters are lost — they arrive at the target job with a literal amp; prepended to their name (e.g. amp;b, amp;c instead of b, c), so the target job does not recognize them as its declared parameters and they fall back to empty/default values.

Inspecting the actual href attribute of the dropdown/jumplist link in DevTools confirms this: for a source URL of /job/target-job/buildWithParameters?a=1&b=2&c=3, the rendered href is:

/job/target-job/buildWithParameters?a=1&b=2&c=3

This is literal text & sitting in the href value (not a decoded &), which is why the browser sends it verbatim in the query string on click, and the server then splits on the literal & inside &, treating amp;b and amp;c as (unknown) parameter names.

In our real-world case, this caused a deploy job's revision parameter to arrive empty, which in turn made a branch('${revision}') SCM step match an arbitrary ref ("Multiple candidate revisions") instead of the intended one — deploying unrelated/old code.

The classic sidebar-rendered link with the same URL works correctly and passes all parameters as expected.

Anything else?

This bug affects any plugin/feature that adds a build/job Action whose getUrlName() returns a URL with more than one query parameter, when that action is accessed through the newer JS-rendered dropdown/jumplist context menus (e.g. the chevron next to a build in the build history widget, or similar /contextMenu-backed overflow menus). It does not affect the classic server-rendered Jelly sidebar.

We noticed this while investigating a regression in the community "Deploy Dashboard" plugin (deploy-dashboard, https://plugins.jenkins.io/deploy-dashboard/) buildAddUrl step, but confirmed via code reading that the plugin itself does no client-side rendering at all (no custom Jelly for its Action) — the URL is rendered entirely by Jenkins core, so this is not a plugin bug.

We also found a related, but distinct, core bug fixed recently in a different overflow-menu component: "Fix breadcrumb overflow dropdown items failing to render" (#26978, merged for 2.571). That fix changed how the breadcrumbs overflow menu passes href/event data (switching from a bare url field to a structured event: {url, type} object), but it did not touch templates.js's menuItem()/optionalVal() double-escaping, which is the mechanism responsible for the bug reported here. We suspect the two are related symptoms of the same broader Bootstrap5/dropdown-menu UI migration (JENKINS-75727 and related), but this specific double-escaping issue appears to still be present as of 2.568.1 (verified in the jenkinsci/jenkins GitHub repo at tag jenkins-2.568.1).

Happy to submit a PR removing the redundant xmlEscape() call if a maintainer can confirm which of the two escaping sites should be removed (i.e. whether optionalVal should skip escaping for pre-escaped fields like href, or whether url construction in menuItem should pass the raw, unescaped string).

Are you interested in contributing a fix?

Yes — the fix looks like a small, low-risk one-line change in src/main/js/components/dropdowns/templates.js (menuItem()), removing one of the two redundant xmlEscape() calls. Happy to submit a PR.

The only thing we'd need guidance on is which of the two escaping sites is the "correct" one to keep, since removing the wrong one could reintroduce an XSS risk instead of just fixing the encoding bug:

  • Option A: keep the escape in optionalVal() (the generic, reusable helper used for all attributes), and change menuItem() to pass the raw itemOptions.event.url (plus context prefix) without pre-escaping it.
  • Option B: keep the escape in menuItem()'s url construction, and have optionalVal() accept a flag (or a pre-escaped marker) to skip re-escaping for that specific value.

We'd lean toward Option A as the more general fix (it keeps optionalVal() as the single source of truth for escaping any attribute value), but would appreciate a maintainer's confirmation before opening a PR, given this touches every dropdown item across the UI (build history, breadcrumbs, context menus, etc.) and we want to avoid a regression.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions