CVE-2019-1003012
Description
Jenkins Blue Ocean Plugins 1.10.1 and earlier allow bypass of CSRF protection, enabling unauthorized data modification.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Blue Ocean Plugins 1.10.1 and earlier allow bypass of CSRF protection, enabling unauthorized data modification.
Vulnerability
Jenkins Blue Ocean Plugins version 1.10.1 and earlier contain a data modification vulnerability in several files including blueocean-core-js/src/js/bundleStartup.js, fetch.ts, and APICrumbExclusion.java, allowing attackers to bypass all cross-site request forgery (CSRF) protection in the Blue Ocean API [2][3].
Exploitation
An attacker can exploit this by tricking a Jenkins user with access to Blue Ocean into visiting a specially crafted web page or link, which then performs unauthorized actions on the Jenkins instance through the Blue Ocean API without proper CSRF token validation [2].
Impact
Successful exploitation allows an attacker to perform arbitrary data modification operations within the Blue Ocean API, effectively impersonating the victim user and potentially altering Jenkins configuration, pipeline data, or other resources [2][3].
Mitigation
The vulnerability is fixed in Jenkins Blue Ocean Plugins version 1.10.2 (or later). Red Hat OpenShift Container Platform 3.11.82 includes the fix [1][4]. Users should upgrade to the latest version of the Blue Ocean plugin or update their OpenShift installation accordingly [2].
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.jenkins.blueocean:blueoceanMaven | < 1.10.2 | 1.10.2 |
Affected products
2- Range: 1.10.1 and earlier
Patches
11a03020b5a50[SECURITY-1201] Add CSRF token to post requests
24 files changed · +270 −87
blueocean-bitbucket-pipeline/src/test/java/io/jenkins/blueocean/blueocean_bitbucket_pipeline/cloud/BbCloudPipelineCreateRequestTest.java+1 −0 modified@@ -21,6 +21,7 @@ public void createPipeline() throws UnirestException, IOException { Map r = new PipelineBaseTest.RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, authenticatedUser.getId(), authenticatedUser.getId())) + .crumb( crumb ) .post("/organizations/jenkins/pipelines/") .data(ImmutableMap.of("name", "pipeline1", "$class", "io.jenkins.blueocean.blueocean_bitbucket_pipeline.BitbucketPipelineCreateRequest", "scmConfig", ImmutableMap.of("id", BitbucketCloudScm.ID, "uri", apiUrl,
blueocean-bitbucket-pipeline/src/test/java/io/jenkins/blueocean/blueocean_bitbucket_pipeline/server/BitbucketPipelineCreateRequestTest.java+3 −0 modified@@ -42,6 +42,7 @@ public void createdWithTraits() throws Exception { Map r = new PipelineBaseTest.RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, authenticatedUser.getId(), authenticatedUser.getId())) + .crumb( crumb ) .post("/organizations/jenkins/pipelines/") .data(ImmutableMap.of( "name","pipeline1", @@ -91,6 +92,7 @@ public void createPipelineBitbucketServerWithCredentialId() throws UnirestExcept Map r = new PipelineBaseTest.RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, authenticatedUser.getId(), authenticatedUser.getId())) + .crumb( crumb ) .post("/organizations/jenkins/pipelines/") .data(ImmutableMap.of( "name","pipeline1", @@ -116,6 +118,7 @@ public void createPipelineBitbucketServerWithoutCredentialId() throws UnirestExc Map r = new PipelineBaseTest.RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, authenticatedUser.getId(), authenticatedUser.getId())) + .crumb( crumb ) .post("/organizations/jenkins/pipelines/") .data(ImmutableMap.of( "name","pipeline1",
blueocean-bitbucket-pipeline/src/test/java/io/jenkins/blueocean/blueocean_bitbucket_pipeline/server/BitbucketServerEndpointTest.java+13 −0 modified@@ -36,6 +36,7 @@ public class BitbucketServerEndpointTest extends BbServerWireMock { public void setup() throws Exception { super.setup(); token = getJwtToken(j.jenkins, authenticatedUser.getId(), authenticatedUser.getId()); + this.crumb = getCrumb( j.jenkins ); } @Test @@ -47,6 +48,7 @@ public void testServerNotBitbucket() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", apiUrl @@ -69,6 +71,7 @@ public void testServerBadServer() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", "http://foobar/" @@ -90,6 +93,7 @@ public void testMissingParams() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of()) .post(URL) .build(Map.class); @@ -110,6 +114,7 @@ public void testMissingUrlParam() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of("name", "foo")) .post(URL) .build(Map.class); @@ -129,6 +134,7 @@ public void testMissingNameParam() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of("apiUrl", apiUrl)) .post(URL) .build(Map.class); @@ -149,6 +155,7 @@ public void avoidDuplicateByUrl() throws Exception { Map server = request() .status(200) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", apiUrl @@ -161,6 +168,7 @@ public void avoidDuplicateByUrl() throws Exception { Map resp = server = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server 2", "apiUrl", apiUrl @@ -185,6 +193,7 @@ public void createAndList() throws Exception { Map server = request() .status(200) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", apiUrl @@ -234,6 +243,7 @@ public void shouldFailOnIncompatibleVersionInAdd() throws UnirestException, IOEx Map server = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", apiUrl @@ -257,6 +267,7 @@ public void shouldFailOnIncompatibleVersion() throws UnirestException, IOExcepti Map server = request() .status(200) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", apiUrl @@ -293,6 +304,7 @@ public void createThenDelete() throws IOException { .as(Void.class); httpRequest().Post(URL) + .header( crumb.field, crumb.value ) .bodyJson(ImmutableMap.of( "name", "My Server", "apiUrl", apiUrl @@ -334,6 +346,7 @@ public void should404OnDeleteNonexistent() throws IOException { private List getServers() { return request() .status(200) + .crumb( crumb ) .jwtToken(token) .get(URL) .build(List.class);
blueocean-core-js/src/js/bundleStartup.js+7 −1 modified@@ -13,8 +13,14 @@ export function execute(done, config) { extensionData: window.$blueocean.jsExtensions, classMetadataProvider: (type, cb) => { const fetch = require('./fetch').Fetch; + const fetchOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }; fetch - .fetchJSON(`${appRoot}/rest/classes/${type}/`) + .fetchJSON(`${appRoot}/rest/classes/${type}/`, { fetchOptions }) .then(cb) .catch(fetch.consoleError); },
blueocean-core-js/src/js/fetch.ts+11 −0 modified@@ -308,6 +308,12 @@ export class Fetch { static fetchJSON(url, { onSuccess, onError, fetchOptions, disableCapabilities, disableLoadingIndicator, ignoreRefreshHeader }: Fetch.FetchOpts = {}) { const fixedUrl = FetchFunctions.prefixUrl(url); let future; + const crumbHeaderName = UrlConfig.getCrumbHeaderName(); + + if (crumbHeaderName && fetchOptions && fetchOptions.headers) { + fetchOptions.headers[crumbHeaderName] = UrlConfig.getCrumbToken(); + } + if (!AppConfig.isJWTEnabled()) { future = FetchFunctions.rawFetchJSON(fixedUrl, { onSuccess, onError, fetchOptions, disableLoadingIndicator, ignoreRefreshHeader }); } else { @@ -341,6 +347,11 @@ export class Fetch { */ static fetch(url, { onSuccess, onError, fetchOptions, disableLoadingIndicator, ignoreRefreshHeader }: Fetch.FetchOpts = {}) { const fixedUrl = FetchFunctions.prefixUrl(url); + const crumbHeaderName = UrlConfig.getCrumbHeaderName(); + + if (crumbHeaderName && fetchOptions && fetchOptions.headers) { + fetchOptions.headers[crumbHeaderName] = UrlConfig.getCrumbToken(); + } if (!AppConfig.isJWTEnabled()) { return FetchFunctions.rawFetch(fixedUrl, { onSuccess, onError, fetchOptions, disableLoadingIndicator, ignoreRefreshHeader });
blueocean-core-js/src/js/i18n/i18n.js+7 −1 modified@@ -42,7 +42,13 @@ function newPluginXHR(pluginName) { if (logger.isDebugEnabled()) { logger.debug('loading data for', url); } - Fetch.fetchJSON(url, { disableCapabilities: true, disableLoadingIndicator: true, ignoreRefreshHeader: true }).then(data => { + const fetchOptions = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + }; + Fetch.fetchJSON(url, { disableCapabilities: true, disableLoadingIndicator: true, ignoreRefreshHeader: true, fetchOptions }).then(data => { callback(data, { status: 200 }); }); },
blueocean-core-js/src/js/rest/RunApi.js+1 −1 modified@@ -1,8 +1,8 @@ /** * Created by cmeyers on 8/29/16. */ -import { Fetch } from '../fetch'; import { UrlConfig } from '../urlconfig'; +import { Fetch } from '../fetch'; import { Utils } from '../utils'; export class RunApi {
blueocean-core-js/src/js/urlconfig.js+28 −0 modified@@ -1,6 +1,8 @@ let jenkinsRootURL = ''; let blueOceanAppURL = '/'; let restBaseURL = ''; +let crumbToken = ''; +let crumbHeaderName = ''; let loaded = false; @@ -23,6 +25,18 @@ function loadConfig() { // typically '/jenkins/blue/rest' restBaseURL = `${blueOceanAppURL}/rest`.replace(/\/\/+/g, '/'); // eliminate any duplicated slashes + // load crumb token used for POST requests + crumbToken = headElement.getAttribute('data-crumbtoken'); + if (typeof crumbToken !== 'string') { + crumbToken = ''; + } + + // load crumb header name used for POST requests + crumbHeaderName = headElement.getAttribute('data-crumbtoken-field'); + if (typeof crumbHeaderName !== 'string') { + crumbHeaderName = ''; + } + loaded = true; } catch (error) { // eslint-disable-next-line no-console @@ -47,6 +61,20 @@ export const UrlConfig = { return blueOceanAppURL; }, + getCrumbHeaderName() { + if (!loaded) { + loadConfig(); + } + return crumbHeaderName; + }, + + getCrumbToken() { + if (!loaded) { + loadConfig(); + } + return crumbToken; + }, + getRestBaseURL() { if (!loaded) { loadConfig();
blueocean-github-pipeline/src/test/java/io/jenkins/blueocean/blueocean_github_pipeline/GithubMockBase.java+4 −0 modified@@ -101,8 +101,10 @@ public void setup() throws Exception { this.user = login("vivek", "Vivek Pandey", "vivek.pandey@gmail.com"); this.githubApiUrl = String.format("http://localhost:%s",githubApi.port()); + this.crumb = getCrumb( j.jenkins ); } + @After public void tearDown() { if (!perTestStubMappings.isEmpty()) { @@ -127,6 +129,7 @@ protected String createGithubCredential(User user) throws UnirestException { .data(ImmutableMap.of("accessToken", accessToken)) .status(200) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .put("/organizations/" + getOrgName() + "/scm/github/validate/?apiUrl="+githubApiUrl) .build(Map.class); String credentialId = (String) r.get("credentialId"); @@ -142,6 +145,7 @@ protected String createGithubEnterpriseCredential(User user) throws UnirestExcep .data(ImmutableMap.of("accessToken", accessToken)) .status(200) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .put("/organizations/" + getOrgName() + "/scm/github-enterprise/validate/?apiUrl="+githubApiUrl) .build(Map.class); String credentialId = (String) r.get("credentialId");
blueocean-github-pipeline/src/test/java/io/jenkins/blueocean/blueocean_github_pipeline/GithubOrgFolderPermissionsTest.java+1 −0 modified@@ -73,6 +73,7 @@ private void createGithubPipeline(boolean shouldSucceed) throws Exception { Map resp = new RequestBuilder(baseUrl) .status(shouldSucceed ? 201 : 403) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(GithubTestUtils.buildRequestBody(GithubScm.ID,null, githubApiUrl, pipelineName, "PR-demo")) .build(Map.class);
blueocean-github-pipeline/src/test/java/io/jenkins/blueocean/blueocean_github_pipeline/GithubPipelineCreateRequestTest.java+10 −0 modified@@ -63,6 +63,7 @@ public void createPipeline() throws UnirestException, IOException { Map r = new PipelineBaseTest.RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/jenkins/pipelines/") .data(ImmutableMap.of("name", "pipeline1", "$class", "io.jenkins.blueocean.blueocean_github_pipeline.GithubPipelineCreateRequest", "scmConfig", ImmutableMap.of("id", GithubScm.ID, "uri", githubApiUrl, "credentialId", credentialId, @@ -138,6 +139,7 @@ public void shouldFailForAnonUserWithCredentialIdMissing() throws Exception { String orgFolderName = "cloudbeers"; Map resp = new RequestBuilder(baseUrl) .status(401) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") .data(GithubTestUtils.buildRequestBody(GithubScm.ID,null, githubApiUrl, orgFolderName, "PR-demo")) .build(Map.class); @@ -152,6 +154,7 @@ public void shouldFailForAnonUserWithCredentialIdSent() throws Exception { String orgFolderName = "cloudbeers"; Map resp = new RequestBuilder(baseUrl) .status(401) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") .data(GithubTestUtils.buildRequestBody(GithubScm.ID, credentialId, githubApiUrl, orgFolderName, "PR-demo")) .build(Map.class); @@ -169,6 +172,7 @@ public void shouldFailForAuthedUserWithoutCredentialCreatedAndCredentialIdMissin Map resp = new RequestBuilder(baseUrl) .status(400) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") .data(GithubTestUtils.buildRequestBody(GithubScm.ID,null, githubApiUrl, orgFolderName, "PR-demo")) .build(Map.class); @@ -186,6 +190,7 @@ public void shouldFailForAuthedUserWithoutCredentialCreatedAndCredentialIdSent() Map resp = new RequestBuilder(baseUrl) .status(400) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") .data(GithubTestUtils.buildRequestBody(GithubScm.ID, credentialId, githubApiUrl, orgFolderName, "PR-demo")) .build(Map.class); @@ -202,6 +207,7 @@ public void shouldSucceedForAuthedUserWithCredentialCreatedAndCredentialIdMissin Map resp = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") // since credentialId will default to 'github', it's okay to omit it in request .data(GithubTestUtils.buildRequestBody(GithubScm.ID, null, githubApiUrl, orgFolderName, "PR-demo")) @@ -219,6 +225,7 @@ public void shouldSucceedForAuthedUserWithCredentialCreatedAndCredentialIdSent() Map resp = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") .data(GithubTestUtils.buildRequestBody(GithubScm.ID, credentialId, githubApiUrl, orgFolderName, "PR-demo")) .build(Map.class); @@ -235,6 +242,7 @@ public void shouldFailForAuthedUserWithCredentialCreatedAndBogusCredentialIdSent Map resp = new RequestBuilder(baseUrl) .status(400) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") .data(GithubTestUtils.buildRequestBody(GithubScm.ID, "bogus-cred", githubApiUrl, orgFolderName, "PR-demo")) .build(Map.class); @@ -251,6 +259,7 @@ public void shouldSucceedForAuthedUserWithCredentialCreatedAndCredentialIdMissin Map resp = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .post("/organizations/"+getOrgName()+"/pipelines/") // since credentialId will default to 'github', it's okay to omit it in request .data(GithubTestUtils.buildRequestBody(GithubEnterpriseScm.ID, null, githubApiUrl, orgFolderName, "PR-demo")) @@ -325,6 +334,7 @@ public void testOrgFolderIndexing() throws IOException, UnirestException { Map map = new RequestBuilder(baseUrl) .post("/organizations/jenkins/pipelines/p/runs/") .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( this.crumb ) .data(ImmutableMap.of()) .status(200) .build(Map.class);
blueocean-github-pipeline/src/test/java/io/jenkins/blueocean/blueocean_github_pipeline/GithubServerTest.java+13 −1 modified@@ -34,10 +34,11 @@ public class GithubServerTest extends PipelineBaseTest { String token; @Before - public void createUser() throws UnirestException { + public void createUser() throws Exception { hudson.model.User user = User.get("alice"); user.setFullName("Alice Cooper"); token = getJwtToken(j.jenkins, "alice", "alice"); + this.crumb = getCrumb( j.jenkins ); } @Test @@ -46,6 +47,7 @@ public void testServerNotGithub() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", getApiUrlCustomPath("/notgithub") @@ -68,6 +70,7 @@ public void testServerGithubEnterpriseTopLevelUrl() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", getApiUrl() @@ -90,6 +93,7 @@ public void testServerUnknownHost() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", "http://foobar/" @@ -111,6 +115,7 @@ public void testMissingParams() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of()) .post("/organizations/jenkins/scm/github-enterprise/servers/") .build(Map.class); @@ -131,6 +136,7 @@ public void testMissingUrlParam() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of("name", "foo")) .post("/organizations/jenkins/scm/github-enterprise/servers/") .build(Map.class); @@ -150,6 +156,7 @@ public void testMissingNameParam() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of("apiUrl", getDefaultApiUrl())) .post("/organizations/jenkins/scm/github-enterprise/servers/") .build(Map.class); @@ -170,6 +177,7 @@ public void avoidDuplicateByUrl() throws Exception { Map server = request() .status(200) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", getDefaultApiUrl() @@ -181,6 +189,7 @@ public void avoidDuplicateByUrl() throws Exception { Map resp = server = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server 2", "apiUrl", getDefaultApiUrl() @@ -203,6 +212,7 @@ public void avoidDuplicateByName() throws Exception { Map server = request() .status(200) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", getDefaultApiUrl() @@ -214,6 +224,7 @@ public void avoidDuplicateByName() throws Exception { Map resp = request() .status(400) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", getDefaultApiUrl() @@ -238,6 +249,7 @@ public void createAndList() throws Exception { Map server = request() .status(200) .jwtToken(token) + .crumb( crumb ) .data(ImmutableMap.of( "name", "My Server", "apiUrl", getDefaultApiUrl()
blueocean-git-pipeline/src/test/java/io/jenkins/blueocean/blueocean_git_pipeline/GitPipelineCreateRequestTest.java+1 −0 modified@@ -45,6 +45,7 @@ public void createPipeline() throws UnirestException, IOException { Map r = new PipelineBaseTest.RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( crumb ) .post("/organizations/jenkins/pipelines/") .data(ImmutableMap.of("name", "pipeline1", "$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest",
blueocean-git-pipeline/src/test/java/io/jenkins/blueocean/blueocean_git_pipeline/GitReadSaveTest.java+9 −0 modified@@ -233,6 +233,7 @@ public void testGitScmValidate() throws Exception { // Validate bob via repositoryUrl Map r = new RequestBuilder(baseUrl) .status(200) + .crumb( crumb ) .jwtToken(getJwtToken(j.jenkins, bob.getId(), bob.getId())) .put("/organizations/" + getOrgName() + "/scm/git/validate/") .data(ImmutableMap.of( @@ -246,6 +247,7 @@ public void testGitScmValidate() throws Exception { String jobName = "test-token-validation"; r = new RequestBuilder(baseUrl) .status(201) + .crumb( crumb ) .jwtToken(getJwtToken(j.jenkins, bob.getId(), bob.getId())) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of( @@ -261,6 +263,7 @@ public void testGitScmValidate() throws Exception { // Test for existing pipeline/job r = new RequestBuilder(baseUrl) .status(200) + .crumb( crumb ) .jwtToken(getJwtToken(j.jenkins, bob.getId(), bob.getId())) .put("/organizations/" + getOrgName() + "/scm/git/validate/") .data(ImmutableMap.of( @@ -273,6 +276,7 @@ public void testGitScmValidate() throws Exception { // Test alice fails r = new RequestBuilder(baseUrl) .status(428) + .crumb( crumb ) .jwtToken(getJwtToken(j.jenkins, alice.getId(), alice.getId())) .put("/organizations/" + getOrgName() + "/scm/git/validate/") .data(ImmutableMap.of( @@ -282,6 +286,7 @@ public void testGitScmValidate() throws Exception { r = new RequestBuilder(baseUrl) .status(428) + .crumb( crumb ) .jwtToken(getJwtToken(j.jenkins, alice.getId(), alice.getId())) .put("/organizations/" + getOrgName() + "/scm/git/validate/") .data(ImmutableMap.of( @@ -331,6 +336,7 @@ private void testGitReadWrite(final @Nonnull GitReadSaveService.ReadSaveType typ Map r = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of( "name", jobName, @@ -347,6 +353,7 @@ private void testGitReadWrite(final @Nonnull GitReadSaveService.ReadSaveType typ r = new RequestBuilder(baseUrl) .status(200) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( crumb ) .get(urlJobPrefix + "/scm/content/?branch=master&path=Jenkinsfile&type="+type.name()) .build(Map.class); @@ -369,6 +376,7 @@ private void testGitReadWrite(final @Nonnull GitReadSaveService.ReadSaveType typ new RequestBuilder(baseUrl) .status(200) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( crumb ) .put(urlJobPrefix + "/scm/content/") .data(ImmutableMap.of("content", content)) .build(Map.class); @@ -382,6 +390,7 @@ private void testGitReadWrite(final @Nonnull GitReadSaveService.ReadSaveType typ // check to make sure we get the same thing from the service r = new RequestBuilder(baseUrl) .status(200) + .crumb( crumb ) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) .get(urlJobPrefix + "/scm/content/?branch=master&path=Jenkinsfile&type="+type.name()) .build(Map.class);
blueocean-git-pipeline/src/test/java/io/jenkins/blueocean/blueocean_git_pipeline/GitScmTest.java+13 −0 modified@@ -67,6 +67,7 @@ private Map createCredentials(User user, Map credRequest) throws UnirestExceptio return new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/credentials/user/") .data(credRequest).build(Map.class); } @@ -101,6 +102,7 @@ public void shouldCreateWithRemoteGitRepo() throws IOException, UnirestException Map r = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of("name", "demo", "$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest", @@ -208,6 +210,7 @@ public void shouldFailForBadCredentialIdOnCreate() throws IOException, UnirestEx resp = new RequestBuilder(baseUrl) .status(400) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of("name", "demo", "$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest", @@ -229,6 +232,7 @@ public void shouldCreateGitMbp() throws IOException, UnirestException { Map resp = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins,"bob", "bob")) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of("name", "demo", "$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest", @@ -282,6 +286,7 @@ public void shouldFailOnValidation3() throws IOException, UnirestException { Map resp = new RequestBuilder(baseUrl) .status(400) .jwtToken(getJwtToken(j.jenkins,"bob", "bob")) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of("name", "demo", "$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest", @@ -306,6 +311,7 @@ public void shouldFailOnValidation4() throws IOException, UnirestException { Map resp = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins,"bob", "bob")) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of("name", "demo", "$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest", @@ -319,6 +325,7 @@ public void shouldFailOnValidation4() throws IOException, UnirestException { resp = new RequestBuilder(baseUrl) .status(400) .jwtToken(getJwtToken(j.jenkins,"bob", "bob")) + .crumb( crumb ) .post("/organizations/" + getOrgName() + "/pipelines/") .data(ImmutableMap.of("name", "demo", "$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest", @@ -368,6 +375,7 @@ public void shouldNotProvideIdForMissingCredentials() throws Exception { Map resp = new RequestBuilder(baseUrl) .status(200) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .get(repoPath) .build(Map.class); @@ -384,6 +392,7 @@ public void shouldBePoliteAboutBadUrl() throws Exception { Map resp = new RequestBuilder(baseUrl) .status(200) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .get(repoPath) .build(Map.class); @@ -409,6 +418,7 @@ public void shouldCreateCredentialsWithDefaultId() throws Exception { Map resp = new RequestBuilder(baseUrl) .status(200) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .data(params) .put(scmValidatePath) .build(Map.class); @@ -422,6 +432,7 @@ public void shouldCreateCredentialsWithDefaultId() throws Exception { Map resp2 = new RequestBuilder(baseUrl) .status(200) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .get(repoPath) .build(Map.class); @@ -451,6 +462,7 @@ public void shouldNotCreateCredentialsForBadUrl1() throws Exception { Map resp = new RequestBuilder(baseUrl) .status(400) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .data(params) .put(scmValidatePath) .build(Map.class); @@ -481,6 +493,7 @@ public void shouldNotCreateCredentialsForBadUrl2() throws Exception { Map resp = new RequestBuilder(baseUrl) .status(428) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .data(params) .put(scmValidatePath) .build(Map.class);
blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/CredentialApiTest.java+25 −0 modified@@ -12,6 +12,11 @@ import com.mashape.unirest.http.exceptions.UnirestException; import hudson.ExtensionList; import hudson.model.User; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.HttpClientBuilder; import org.junit.Assert; import org.junit.Test; @@ -129,6 +134,7 @@ public void createUsingUsernamePasswordInUserStore() throws IOException, Unirest Map resp = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .post("/organizations/jenkins/credentials/user/") .data( ImmutableMap.of("credentials", new ImmutableMap.Builder<String,Object>() @@ -153,6 +159,7 @@ public void createSshCredentialUsingDirectSshInUserStore() throws IOException, U Map resp = new RequestBuilder(baseUrl) .status(201) .jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId())) + .crumb( crumb ) .post("/organizations/jenkins/credentials/user/") .data( ImmutableMap.of("credentials", new ImmutableMap.Builder<String,Object>() @@ -186,4 +193,22 @@ public void createSshCredentialUsingDirectSshInUserStore() throws IOException, U Assert.assertEquals("SSH Username with private key", resp.get("typeName")); Assert.assertEquals("blueocean-git-domain", resp.get("domain")); } + + + @Test + public void crumbRejected() throws IOException, UnirestException { + User user = login(); + HttpClient httpClient = HttpClientBuilder.create().build(); + + HttpPost post = new HttpPost( baseUrl + "/organizations/jenkins/credentials/user/" ); + post.addHeader( "Authorization", "Bearer " + getJwtToken(j.jenkins,user.getId(), user.getId())); + + HttpResponse resp = httpClient.execute( post ); + Assert.assertEquals(403, resp.getStatusLine().getStatusCode()); + //LOGGER.info( IOUtils.toString( resp.getEntity().getContent() )); + // assert content contains No valid crumb was included in the request + // olamy not sure as it can be i18n sensitive + } + + }
blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchTest.java+1 −0 modified@@ -329,6 +329,7 @@ public void multiBranchPipelineIndex() throws Exception { Map map = new RequestBuilder(baseUrl) .post("/organizations/jenkins/pipelines/p/runs/") .jwtToken(getJwtToken(j.jenkins, user.getId(), user.getId())) + .crumb( getCrumb( j.jenkins ) ) .data(ImmutableMap.of()) .status(200) .build(Map.class);
blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/PipelineBaseTest.java+66 −33 modified@@ -14,6 +14,7 @@ import hudson.model.Job; import hudson.model.Run; import hudson.model.User; +import hudson.security.csrf.CrumbIssuer; import hudson.tasks.Mailer; import io.jenkins.blueocean.commons.JsonConverter; import jenkins.model.Jenkins; @@ -77,6 +78,8 @@ public static void resetJWT() { protected String jwtToken; + protected Crumb crumb; + protected String getContextPath(){ return "blue/rest"; } @@ -89,6 +92,8 @@ public void setup() throws Exception { } this.baseUrl = j.jenkins.getRootUrl() + getContextPath(); this.jwtToken = getJwtToken(j.jenkins); + this.crumb = getCrumb( j.jenkins ); + Unirest.setObjectMapper(new ObjectMapper() { public <T> T readValue(String value, Class<T> valueType) { try { @@ -121,6 +126,19 @@ public String writeValue(Object value) { // Unirest.setHttpClient(); } + protected static class Crumb { + public String field, value; + } + + public static Crumb getCrumb(Jenkins jenkins) throws Exception { + + Crumb crumb = new Crumb(); + CrumbIssuer crumbIssuer = jenkins.getCrumbIssuer(); + crumb.field = crumbIssuer.getCrumbRequestField(); + crumb.value = crumbIssuer.getCrumb(); + return crumb; + } + protected <T> T get(String path, Class<T> type){ return get(path,200, type); } @@ -180,6 +198,7 @@ protected Map<String, Object> post(String path, Object body, int expectedStatus) HttpResponse<Map> response = Unirest.post(getBaseUrl(path)) .header("Content-Type","application/json") .header("Authorization", "Bearer "+jwtToken) + .header( crumb.field, crumb.value ) .body(body).asObject(Map.class); Assert.assertEquals(expectedStatus, response.getStatus()); return response.getBody(); @@ -419,6 +438,7 @@ public class RequestBuilder { private String baseUrl; private int expectedStatus = 200; private String token; + private Crumb crumb; private Map<String,String> headers = new HashMap<>(); @@ -457,6 +477,11 @@ public RequestBuilder jwtToken(String token){ return this; } + public RequestBuilder crumb(Crumb crumb){ + this.crumb = crumb; + return this; + } + public RequestBuilder data(Map data) { this.data = data; return this; @@ -498,50 +523,58 @@ public RequestBuilder delete(String url) { return this; } - public <T> T build(Class<T> clzzz) { + public HttpRequest build() { assert url != null; assert url.startsWith("/"); - try { - HttpRequest request; - switch (method) { - case "PUT": - request = Unirest.put(getBaseUrl(url)); - break; - case "POST": - request = Unirest.post(getBaseUrl(url)); - break; - case "GET": - request = Unirest.get(getBaseUrl(url)); - break; - case "DELETE": - request = Unirest.delete(getBaseUrl(url)); - break; - default: - throw new RuntimeException("No default options"); - } - request.header("Accept-Encoding",""); - if(!Strings.isNullOrEmpty(username) && !Strings.isNullOrEmpty(password)){ - request.basicAuth(username, password); + HttpRequest request; + switch (method) { + case "PUT": + request = Unirest.put(getBaseUrl(url)); + break; + case "POST": + request = Unirest.post(getBaseUrl(url)); + break; + case "GET": + request = Unirest.get(getBaseUrl(url)); + break; + case "DELETE": + request = Unirest.delete(getBaseUrl(url)); + break; + default: + throw new RuntimeException("No default options"); + + } + if(crumb!=null){ + request.header( crumb.field, crumb.value ); + } + request.header("Accept-Encoding",""); + if(!Strings.isNullOrEmpty(username) && !Strings.isNullOrEmpty(password)){ + request.basicAuth(username, password); + }else{ + if (token == null) { + request.header("Authorization", "Bearer " + PipelineBaseTest.this.jwtToken); }else{ - if (token == null) { - request.header("Authorization", "Bearer " + PipelineBaseTest.this.jwtToken); - }else{ - request.header("Authorization", "Bearer " + token); - } + request.header("Authorization", "Bearer " + token); } + } - request.header("Content-Type", contentType); + request.header("Content-Type", contentType); - request.headers(headers); + request.headers(headers); - if(request instanceof HttpRequestWithBody && data != null) { - ((HttpRequestWithBody)request).body(data); - } + if(request instanceof HttpRequestWithBody && data != null) { + ((HttpRequestWithBody)request).body(data); + } + return request; + } + public <T> T build(Class<T> clzzz) { + try { + HttpRequest request = build(); HttpResponse<T> response = request.asObject(clzzz); Assert.assertEquals(response.getStatusText(), expectedStatus, response.getStatus()); - return response.getBody(); + return response.getBody(); } catch (UnirestException e) { throw new RuntimeException(e); }
blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/RunImplTest.java+2 −1 modified@@ -210,7 +210,8 @@ public void pipelineLatestRunIncludesRunning() throws Exception { String replayURL = String.format("/organizations/jenkins/pipelines/%s/runs/%s/replay/", p.getName(), idOfSecondRun); try { Thread.sleep(200); - request().post(replayURL).build(String.class); + + request().crumb( getCrumb( j.jenkins ) ).post(replayURL).build(String.class); } catch (Exception e) { Thread.sleep(200); request().post(replayURL).build(String.class);
blueocean-rest-impl/src/test/java/io/jenkins/blueocean/service/embedded/BaseTest.java+29 −0 modified@@ -12,6 +12,8 @@ import hudson.model.Job; import hudson.model.Run; import hudson.model.User; +import hudson.security.csrf.CrumbIssuer; +import hudson.security.csrf.DefaultCrumbIssuer; import hudson.tasks.Mailer; import io.jenkins.blueocean.commons.JsonConverter; import jenkins.model.Jenkins; @@ -53,6 +55,8 @@ protected String getContextPath(){ protected String jwtToken; + protected Crumb crumb; + @Before public void setup() throws Exception { if(System.getProperty("DISABLE_HTTP_HEADER_TRACE") == null) { @@ -61,6 +65,7 @@ public void setup() throws Exception { } this.baseUrl = j.jenkins.getRootUrl() + getContextPath(); this.jwtToken = getJwtToken(j.jenkins); + this.crumb = getCrumb( j.jenkins ); Unirest.setObjectMapper(new ObjectMapper() { public <T> T readValue(String value, Class<T> valueType) { try { @@ -153,6 +158,7 @@ protected Map<String, Object> post(String path, Object body, int expectedStatus) HttpResponse<Map> response = Unirest.post(getBaseUrl(path)) .header("Content-Type","application/json") .header("Authorization", "Bearer "+jwtToken) + .header( crumb.field, crumb.value ) .body(body).asObject(Map.class); Assert.assertEquals(expectedStatus, response.getStatus()); return response.getBody(); @@ -321,6 +327,7 @@ public class RequestBuilder { private String contentType = "application/json"; private String baseUrl; private int expectedStatus = 200; + private Crumb crumb; private String token; @@ -359,6 +366,11 @@ public RequestBuilder jwtToken(String token){ return this; } + public RequestBuilder crumb(Crumb crumb){ + this.crumb = crumb; + return this; + } + public RequestBuilder data(Map data) { this.data = data; @@ -433,6 +445,10 @@ public <T> HttpResponse<T> execute(Class<T> clzzz) { request.basicAuth(username, password); } + if(crumb!=null){ + request.header( crumb.field, crumb.value ); + } + if(token == null) { request.header("Authorization", "Bearer " + BaseTest.this.jwtToken); }else{ @@ -466,6 +482,19 @@ public static String getJwtToken(Jenkins jenkins) throws UnirestException { return token; } + static class Crumb { + String field, value; + } + + public static Crumb getCrumb(Jenkins jenkins) throws Exception { + + Crumb crumb = new Crumb(); + CrumbIssuer crumbIssuer = jenkins.getCrumbIssuer(); + crumb.field = crumbIssuer.getCrumbRequestField(); + crumb.value = crumbIssuer.getCrumb(); + return crumb; + } + public static String getJwtToken(Jenkins jenkins, String username, String password) throws UnirestException { GetRequest request = Unirest.get(jenkins.getRootUrl()+"jwt-auth/token/").header("Accept", "*/*") .header("Accept-Encoding","");
blueocean-rest-impl/src/test/java/io/jenkins/blueocean/service/embedded/PipelineApiTest.java+1 −1 modified@@ -556,7 +556,7 @@ public void testNewPipelineQueueItem() throws Exception { p2.scheduleBuild2(0).waitForStart(); // Run the third pipeline - Map r = request().post("/organizations/jenkins/pipelines/pipeline3/runs/").build(Map.class); + Map r = request().crumb( crumb ).post("/organizations/jenkins/pipelines/pipeline3/runs/").build(Map.class); // Ensure it is still in the queue assertNotNull(p3.getQueueItem());
blueocean-rest/src/main/java/io/jenkins/blueocean/rest/APICrumbExclusion.java+0 −47 modified@@ -1,47 +0,0 @@ -package io.jenkins.blueocean.rest; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import hudson.Extension; -import hudson.ExtensionList; -import hudson.security.csrf.CrumbExclusion; -import io.jenkins.blueocean.RootRoutable; - -/** - * This class forces the Blueocean API to require json for POSTs so that we do not need a crumb. - * @author Ivan Meredith - */ -@Extension -public class APICrumbExclusion extends CrumbExclusion{ - @Override - public boolean process(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws IOException, ServletException { - String pathInfo = httpServletRequest.getPathInfo(); - - for (RootRoutable r : ExtensionList.lookup(RootRoutable.class)) { - String path = getExclusionPath(r.getUrlName()); - if (pathInfo != null && pathInfo.startsWith(path)) { - String header = httpServletRequest.getHeader("Content-Type"); - if(header != null && header.contains("application/json")) { - filterChain.doFilter(httpServletRequest, httpServletResponse); - return true; - } else { - return false; - } - - } - } - - return false; - - } - - public String getExclusionPath(String route) { - return "/blue/" + route + "/"; - } - -}
blueocean-web/src/main/java/io/jenkins/blueocean/BlueOceanUI.java+21 −0 modified@@ -2,7 +2,10 @@ import hudson.ExtensionList; import hudson.Main; +import hudson.security.csrf.CrumbIssuer; import io.jenkins.blueocean.dev.RunBundleWatches; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.slf4j.Logger; @@ -79,6 +82,24 @@ public String getLang() { return null; } + /** + * Get the crumb token value + * @return the crumb token value or empty String if no {@link CrumbIssuer} + */ + public String getCrumbToken() { + CrumbIssuer crumbIssuer = Jenkins.get().getCrumbIssuer(); + return crumbIssuer == null ? StringUtils.EMPTY : crumbIssuer.getCrumb(); + } + + /** + * Get the crumb request field + * @return the crumb request field or empty String if no {@link CrumbIssuer} + */ + public String getCrumbRequestField() { + CrumbIssuer crumbIssuer = Jenkins.get().getCrumbIssuer(); + return crumbIssuer == null ? StringUtils.EMPTY : crumbIssuer.getCrumbRequestField(); + } + public List<BluePageDecorator> getPageDecorators(){ return BluePageDecorator.all(); }
blueocean-web/src/main/resources/io/jenkins/blueocean/BlueOceanUI/index.jelly+3 −1 modified@@ -21,7 +21,9 @@ data-resurl="${resURL}" data-appurl="${rootURL}/${it.urlBase}" data-servertime="${it.now}" - data-adjuncturl="${rootURL}/${j.getAdjuncts('').rootURL}"> + data-adjuncturl="${rootURL}/${j.getAdjuncts('').rootURL}" + data-crumbtoken-field="${it.crumbRequestField}" + data-crumbtoken="${it.crumbToken}"> <title>Jenkins</title>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- access.redhat.com/errata/RHBA-2019:0326ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHBA-2019:0327ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-qxh5-5r5p-5gvfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-1003012ghsaADVISORY
- github.com/jenkinsci/blueocean-plugin/commit/1a03020b5a50c1e3f47d4b0902ec7fc78d3c86ceghsaWEB
- jenkins.io/security/advisory/2019-01-28/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.