Commit 1eb01533 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'security-jobs-view-xss' into 'master'

Fix XSS on jobs view

Closes #176

See merge request gitlab-org/security/gitlab!611
parents afdd2a1e ca88e653
<script>
import { escape, isEmpty } from 'lodash';
import { isEmpty } from 'lodash';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
import { __ } from '../../locale';
import { GlSprintf, GlLink } from '@gitlab/ui';
export default {
creatingEnvironment: 'creating',
components: {
CiIcon,
GlSprintf,
GlLink,
},
props: {
deploymentStatus: {
......@@ -31,7 +35,7 @@ export default {
return this.outOfDateEnvironmentMessage();
case 'failed':
return this.failedEnvironmentMessage();
case 'creating':
case this.$options.creatingEnvironment:
return this.creatingEnvironmentMessage();
default:
return '';
......@@ -39,17 +43,12 @@ export default {
},
environmentLink() {
if (this.hasEnvironment) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.deploymentStatus.environment.environment_path}" class="js-environment-link">`,
name: escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
return {
link: this.deploymentStatus.environment.environment_path,
name: this.deploymentStatus.environment.name,
};
}
return '';
return {};
},
hasLastDeployment() {
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
......@@ -74,201 +73,107 @@ export default {
}
const { name, path } = this.deploymentCluster;
const escapedName = escape(name);
const escapedPath = escape(path);
if (!escapedPath) {
return escapedName;
}
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${escapedPath}" class="js-job-cluster-link">`,
name: escapedName,
endLink: '</a>',
},
false,
);
return {
path,
name,
};
},
kubernetesNamespace() {
return this.hasCluster ? this.deploymentCluster.kubernetes_namespace : null;
},
deploymentLink() {
return {
path: this.lastDeploymentPath,
name:
this.deploymentStatus.status === this.$options.creatingEnvironment
? __('latest deployment')
: __('most recent deployment'),
};
},
},
methods: {
deploymentLink(name) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`,
name,
endLink: '</a>',
},
false,
);
},
failedEnvironmentMessage() {
const { environmentLink } = this;
return sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
{ environmentLink },
false,
);
return __('The deployment of this job to %{environmentLink} did not succeed.');
},
lastEnvironmentMessage() {
const { environmentLink, clusterNameOrLink, hasCluster, kubernetesNamespace } = this;
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace },
false,
if (this.hasCluster) {
if (this.kubernetesNamespace) {
return __(
'This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
);
}
// we know the cluster but not the namespace
return sprintf(
__('This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}.'),
{ environmentLink, clusterNameOrLink },
false,
);
return __('This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}.');
}
// not a cluster deployment
return sprintf(__('This job is deployed to %{environmentLink}.'), { environmentLink }, false);
return __('This job is deployed to %{environmentLink}.');
},
outOfDateEnvironmentMessage() {
const {
hasLastDeployment,
hasCluster,
environmentLink,
clusterNameOrLink,
kubernetesNamespace,
} = this;
if (hasLastDeployment) {
const deploymentLink = this.deploymentLink(__('most recent deployment'));
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. View the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace, deploymentLink },
false,
if (this.hasLastDeployment) {
if (this.hasCluster) {
if (this.kubernetesNamespace) {
return __(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. View the %{deploymentLink}.',
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, deploymentLink },
false,
return __(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}.',
);
}
// not a cluster deployment
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}.',
),
{ environmentLink, deploymentLink },
false,
return __(
'This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}.',
);
}
// no last deployment, i.e. this is the first deployment
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace },
false,
if (this.hasCluster) {
if (this.kubernetesNamespace) {
return __(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}.',
),
{ environmentLink, clusterNameOrLink },
false,
return __(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}.',
);
}
// not a cluster deployment
return sprintf(
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink },
false,
);
return __('This job is an out-of-date deployment to %{environmentLink}.');
},
creatingEnvironmentMessage() {
const {
hasLastDeployment,
hasCluster,
environmentLink,
clusterNameOrLink,
kubernetesNamespace,
} = this;
if (hasLastDeployment) {
const deploymentLink = this.deploymentLink(__('latest deployment'));
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. This will overwrite the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace, deploymentLink },
false,
if (this.hasLastDeployment) {
if (this.hasCluster) {
if (this.kubernetesNamespace) {
return __(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. This will overwrite the %{deploymentLink}.',
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, deploymentLink },
false,
return __(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}.',
);
}
// not a cluster deployment
return sprintf(
__(
'This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}.',
),
{ environmentLink, deploymentLink },
false,
return __(
'This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}.',
);
}
// no last deployment, i.e. this is the first deployment
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace },
false,
if (this.hasCluster) {
if (this.kubernetesNamespace) {
return __(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}.',
),
{ environmentLink, clusterNameOrLink },
false,
return __(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}.',
);
}
// not a cluster deployment
return sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink },
false,
);
return __('This job is creating a deployment to %{environmentLink}.');
},
},
};
......@@ -277,7 +182,37 @@ export default {
<div class="gl-mt-3 gl-mb-3 js-environment-container">
<div class="environment-information">
<ci-icon :status="iconStatus" />
<p class="inline gl-mb-0" v-html="environment"></p>
<p class="inline gl-mb-0">
<gl-sprintf :message="environment">
<template #environmentLink>
<gl-link
v-if="hasEnvironment"
:href="environmentLink.link"
data-testid="job-environment-link"
v-text="environmentLink.name"
/>
</template>
<template #clusterNameOrLink>
<gl-link
v-if="clusterNameOrLink.path"
:href="clusterNameOrLink.path"
data-testid="job-cluster-link"
v-text="clusterNameOrLink.name"
/>
<template v-else>{{ clusterNameOrLink.name }}</template>
</template>
<template #kubernetesNamespace>
<template>{{ kubernetesNamespace }}</template>
</template>
<template #deploymentLink>
<gl-link
:href="deploymentLink.path"
data-testid="job-deployment-link"
v-text="deploymentLink.name"
/>
</template>
</gl-sprintf>
</p>
</div>
</div>
</template>
---
title: Fix xss vulnerability on jobs view
merge_request:
author:
type: security
......@@ -551,7 +551,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'shows deployment message' do
expect(page).to have_content 'This job is deployed to production'
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
expect(find('[data-testid="job-environment-link"]')['href']).to match("environments/#{environment.id}")
end
context 'when there is a cluster used for the deployment' do
......@@ -583,7 +583,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'shows a link for the job' do
expect(page).to have_link environment.name
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
expect(find('[data-testid="job-environment-link"]')['href']).to match("environments/#{environment.id}")
end
end
......@@ -593,7 +593,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'shows a link to latest deployment' do
expect(page).to have_link environment.name
expect(page).to have_content 'This job is creating a deployment'
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
expect(find('[data-testid="job-environment-link"]')['href']).to match("environments/#{environment.id}")
end
end
end
......@@ -645,15 +645,15 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'renders a link to the most recent deployment' do
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
expect(find('.js-job-deployment-link')['href']).to include(second_deployment.deployable.project.path, second_deployment.deployable_id.to_s)
expect(find('[data-testid="job-environment-link"]')['href']).to match("environments/#{environment.id}")
expect(find('[data-testid="job-deployment-link"]')['href']).to include(second_deployment.deployable.project.path, second_deployment.deployable_id.to_s)
end
context 'when deployment does not have a deployable' do
let!(:second_deployment) { create(:deployment, :success, environment: environment, deployable: nil) }
it 'has an empty href' do
expect(find('.js-job-deployment-link')['href']).to be_empty
expect(find('[data-testid="job-deployment-link"]')['href']).to be_empty
end
end
end
......@@ -679,7 +679,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
expected_text = 'This job is creating a deployment to staging'
expect(page).to have_css('.environment-information', text: expected_text)
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
expect(find('[data-testid="job-environment-link"]')['href']).to match("environments/#{environment.id}")
end
context 'when it has deployment' do
......@@ -690,7 +690,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
expect(page).to have_css('.environment-information', text: expected_text)
expect(page).to have_css('.environment-information', text: 'latest deployment')
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
expect(find('[data-testid="job-environment-link"]')['href']).to match("environments/#{environment.id}")
end
end
end
......@@ -705,7 +705,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
'.environment-information', text: expected_text)
expect(page).not_to have_css(
'.environment-information', text: 'latest deployment')
expect(find('.js-environment-link')['href']).to match("environments/#{environment.id}")
expect(find('[data-testid="job-environment-link"]')['href']).to match("environments/#{environment.id}")
end
end
end
......
import Vue from 'vue';
import component from '~/jobs/components/environments_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import { mount } from '@vue/test-utils';
import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
const TEST_CLUSTER_NAME = 'test_cluster';
const TEST_CLUSTER_PATH = 'path/to/test_cluster';
const TEST_KUBERNETES_NAMESPACE = 'this-is-a-kubernetes-namespace';
describe('Environments block', () => {
const Component = Vue.extend(component);
let vm;
let wrapper;
const status = {
group: 'success',
icon: 'status_success',
......@@ -38,20 +37,23 @@ describe('Environments block', () => {
});
const createComponent = (deploymentStatus = {}, deploymentCluster = {}) => {
vm = mountComponent(Component, {
deploymentStatus,
deploymentCluster,
iconStatus: status,
wrapper = mount(EnvironmentsBlock, {
propsData: {
deploymentStatus,
deploymentCluster,
iconStatus: status,
},
});
};
const findText = () => vm.$el.textContent.trim();
const findJobDeploymentLink = () => vm.$el.querySelector('.js-job-deployment-link');
const findEnvironmentLink = () => vm.$el.querySelector('.js-environment-link');
const findClusterLink = () => vm.$el.querySelector('.js-job-cluster-link');
const findText = () => wrapper.find(EnvironmentsBlock).text();
const findJobDeploymentLink = () => wrapper.find('[data-testid="job-deployment-link"]');
const findEnvironmentLink = () => wrapper.find('[data-testid="job-environment-link"]');
const findClusterLink = () => wrapper.find('[data-testid="job-cluster-link"]');
afterEach(() => {
vm.$destroy();
wrapper.destroy();
wrapper = null;
});
describe('with last deployment', () => {
......@@ -61,7 +63,7 @@ describe('Environments block', () => {
environment,
});
expect(findText()).toEqual('This job is deployed to environment.');
expect(findText()).toBe('This job is deployed to environment.');
});
describe('when there is a cluster', () => {
......@@ -74,7 +76,7 @@ describe('Environments block', () => {
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
expect(findText()).toBe(
`This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
});
......@@ -89,7 +91,7 @@ describe('Environments block', () => {
createDeploymentWithClusterAndKubernetesNamespace(),
);
expect(findText()).toEqual(
expect(findText()).toBe(
`This job is deployed to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}.`,
);
});
......@@ -105,11 +107,11 @@ describe('Environments block', () => {
environment: createEnvironmentWithLastDeployment(),
});
expect(findText()).toEqual(
expect(findText()).toBe(
'This job is an out-of-date deployment to environment. View the most recent deployment.',
);
expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar');
expect(findJobDeploymentLink().attributes('href')).toBe('bar');
});
describe('when there is a cluster', () => {
......@@ -122,7 +124,7 @@ describe('Environments block', () => {
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
expect(findText()).toBe(
`This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`,
);
});
......@@ -137,7 +139,7 @@ describe('Environments block', () => {
createDeploymentWithClusterAndKubernetesNamespace(),
);
expect(findText()).toEqual(
expect(findText()).toBe(
`This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}. View the most recent deployment.`,
);
});
......@@ -152,7 +154,7 @@ describe('Environments block', () => {
environment,
});
expect(findText()).toEqual('This job is an out-of-date deployment to environment.');
expect(findText()).toBe('This job is an out-of-date deployment to environment.');
});
});
});
......@@ -164,7 +166,7 @@ describe('Environments block', () => {
environment,
});
expect(findText()).toEqual('The deployment of this job to environment did not succeed.');
expect(findText()).toBe('The deployment of this job to environment did not succeed.');
});
});
......@@ -176,13 +178,15 @@ describe('Environments block', () => {
environment: createEnvironmentWithLastDeployment(),
});
expect(findText()).toEqual(
expect(findText()).toBe(
'This job is creating a deployment to environment. This will overwrite the latest deployment.',
);
expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar');
expect(findEnvironmentLink().getAttribute('href')).toEqual(environment.environment_path);
expect(findClusterLink()).toBeNull();
expect(findEnvironmentLink().attributes('href')).toBe(environment.environment_path);
expect(findJobDeploymentLink().attributes('href')).toBe('bar');
expect(findClusterLink().exists()).toBe(false);
});
});
......@@ -193,7 +197,7 @@ describe('Environments block', () => {
environment,
});
expect(findText()).toEqual('This job is creating a deployment to environment.');
expect(findText()).toBe('This job is creating a deployment to environment.');
});
describe('when there is a cluster', () => {
......@@ -206,7 +210,7 @@ describe('Environments block', () => {
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
expect(findText()).toBe(
`This job is creating a deployment to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
});
......@@ -220,7 +224,7 @@ describe('Environments block', () => {
environment: null,
});
expect(findEnvironmentLink()).toBeNull();
expect(findEnvironmentLink().exists()).toBe(false);
});
});
});
......@@ -235,11 +239,11 @@ describe('Environments block', () => {
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
expect(findText()).toBe(
`This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
expect(findClusterLink().getAttribute('href')).toEqual(TEST_CLUSTER_PATH);
expect(findClusterLink().attributes('href')).toBe(TEST_CLUSTER_PATH);
});
describe('when the cluster is missing the path', () => {
......@@ -254,7 +258,7 @@ describe('Environments block', () => {
expect(findText()).toContain('using cluster the-cluster.');
expect(findClusterLink()).toBeNull();
expect(findClusterLink().exists()).toBe(false);
});
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment