Skip to content
Snippets Groups Projects
Commit dc4b3c00 authored by Chad Woolley's avatar Chad Woolley Committed by GitLab Release Tools Bot
Browse files

Warn when snippet contains unretrievable files

Merge branch 'security-snippets-warn-unretrievable-files-14-7' into '14-7-stable-ee'

See merge request gitlab-org/security/gitlab!2204

Changelog: security
parent dfd1d9f3
No related branches found
No related tags found
No related merge requests found
Showing
with 119 additions and 19 deletions
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub';
import {
SNIPPET_MARK_VIEW_APP_START,
Loading
Loading
@@ -23,6 +23,7 @@ export default {
EmbedDropdown,
SnippetHeader,
SnippetTitle,
GlAlert,
GlLoadingIcon,
SnippetBlob,
CloneDropdownButton,
Loading
Loading
@@ -35,6 +36,9 @@ export default {
canBeCloned() {
return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo);
},
hasUnretrievableBlobs() {
return this.snippet.hasUnretrievableBlobs;
},
},
beforeCreate() {
performanceMarkAndMeasure({ mark: SNIPPET_MARK_VIEW_APP_START });
Loading
Loading
@@ -66,6 +70,13 @@ export default {
data-qa-selector="clone_button"
/>
</div>
<gl-alert v-if="hasUnretrievableBlobs" variant="danger" class="gl-mb-3" :dismissible="false">
{{
__(
'WARNING: This snippet contains hidden files which might be used to mask malicious behavior. Exercise caution if cloning and executing code from this snippet.',
)
}}
</gl-alert>
<snippet-blob
v-for="blob in blobs"
:key="blob.path"
Loading
Loading
Loading
Loading
@@ -17,6 +17,7 @@ export const getSnippetMixin = {
 
// Set `snippet.blobs` since some child components are coupled to this.
if (!isEmpty(res)) {
res.hasUnretrievableBlobs = res.blobs?.hasUnretrievableBlobs || false;
// It's possible for us to not get any blobs in a response.
// In this case, we should default to current blobs.
res.blobs = res.blobs ? res.blobs.nodes : blobsDefault;
Loading
Loading
Loading
Loading
@@ -15,6 +15,7 @@ query GetSnippetQuery($ids: [SnippetID!]) {
sshUrlToRepo
blobs {
__typename
hasUnretrievableBlobs
nodes {
__typename
binary
Loading
Loading
Loading
Loading
@@ -12,6 +12,7 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
richData @include(if: $rich)
plainData @skip(if: $rich)
}
hasUnretrievableBlobs
}
}
}
Loading
Loading
Loading
Loading
@@ -19,18 +19,18 @@ module Resolvers
def resolve(paths: [])
return [snippet.blob] if snippet.empty_repo?
 
if paths.empty?
snippet.blobs
else
snippet.repository.blobs_at(transformed_blob_paths(paths))
end
end
private
def transformed_blob_paths(paths)
ref = snippet.default_branch
paths.map { |path| [ref, path] }
paths = snippet.all_files if paths.empty?
blobs = snippet.blobs(paths)
# TODO: Some blobs, e.g. those with non-utf8 filenames, are returned as nil from the
# repository. We need to provide a flag to notify the user of this until we come up with a
# way to retrieve and display these blobs. We will be exploring a more holistic solution for
# this general problem of making all blobs retrievable as part
# of https://gitlab.com/gitlab-org/gitlab/-/issues/323082, at which point this attribute may
# be removed.
context[:unretrievable_blobs?] = blobs.size < paths.size
blobs
end
end
end
Loading
Loading
# frozen_string_literal: true
module Types
module Snippets
# rubocop: disable Graphql/AuthorizeTypes
class BlobConnectionType < GraphQL::Types::Relay::BaseConnection
field :has_unretrievable_blobs, GraphQL::Types::Boolean, null: false,
description: 'Indicates if the snippet has unretrievable blobs.',
resolver_method: :unretrievable_blobs?
def unretrievable_blobs?
!!context[:unretrievable_blobs?]
end
end
end
end
Loading
Loading
@@ -8,6 +8,8 @@ module Types
description 'Represents the snippet blob'
present_using SnippetBlobPresenter
 
connection_type_class(Types::Snippets::BlobConnectionType)
field :rich_data, GraphQL::Types::String,
description: 'Blob highlighted data.',
null: true
Loading
Loading
Loading
Loading
@@ -237,15 +237,19 @@ class Snippet < ApplicationRecord
end
end
 
def all_files
list_files(default_branch)
end
def blob
@blob ||= Blob.decorate(SnippetBlob.new(self), self)
end
 
def blobs
def blobs(paths = [])
return [] unless repository_exists?
 
files = list_files(default_branch)
items = files.map { |file| [default_branch, file] }
paths = all_files if paths.empty?
items = paths.map { |path| [default_branch, path] }
 
repository.blobs_at(items).compact
end
Loading
Loading
Loading
Loading
@@ -7675,6 +7675,7 @@ The connection type for [`SnippetBlob`](#snippetblob).
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="snippetblobconnectionedges"></a>`edges` | [`[SnippetBlobEdge]`](#snippetblobedge) | A list of edges. |
| <a id="snippetblobconnectionhasunretrievableblobs"></a>`hasUnretrievableBlobs` | [`Boolean!`](#boolean) | Indicates if the snippet has unretrievable blobs. |
| <a id="snippetblobconnectionnodes"></a>`nodes` | [`[SnippetBlob]`](#snippetblob) | A list of nodes. |
| <a id="snippetblobconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
 
Loading
Loading
@@ -39848,6 +39848,9 @@ msgstr ""
msgid "WARNING:"
msgstr ""
 
msgid "WARNING: This snippet contains hidden files which might be used to mask malicious behavior. Exercise caution if cloning and executing code from this snippet."
msgstr ""
msgid "Wait for the file to load to copy its contents"
msgstr ""
 
Loading
Loading
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
Loading
Loading
@@ -106,6 +106,23 @@ describe('Snippet view app', () => {
});
});
 
describe('hasUnretrievableBlobs alert rendering', () => {
it.each`
hasUnretrievableBlobs | condition | isRendered
${false} | ${'not render'} | ${false}
${true} | ${'render'} | ${true}
`('does $condition gl-alert by default', ({ hasUnretrievableBlobs, isRendered }) => {
createComponent({
data: {
snippet: {
hasUnretrievableBlobs,
},
},
});
expect(wrapper.findComponent(GlAlert).exists()).toBe(isRendered);
});
});
describe('Clone button rendering', () => {
it.each`
httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered
Loading
Loading
Loading
Loading
@@ -13,11 +13,14 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
let_it_be(:current_user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) }
 
let(:query_context) { {} }
context 'when user is not authorized' do
let(:other_user) { create(:user) }
 
it 'redacts the field' do
expect(resolve_blobs(snippet, user: other_user)).to be_nil
expect(query_context[:unretrievable_blobs?]).to eq(false)
end
end
 
Loading
Loading
@@ -28,6 +31,7 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
expect(result).to match_array(snippet.list_files.map do |file|
have_attributes(path: file)
end)
expect(query_context[:unretrievable_blobs?]).to eq(false)
end
end
 
Loading
Loading
@@ -37,12 +41,14 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
path = 'CHANGELOG'
 
expect(resolve_blobs(snippet, paths: [path])).to contain_exactly(have_attributes(path: path))
expect(query_context[:unretrievable_blobs?]).to eq(false)
end
end
 
context 'the argument does not match anything' do
it 'returns an empty result' do
expect(resolve_blobs(snippet, paths: ['does not exist'])).to be_empty
expect(query_context[:unretrievable_blobs?]).to eq(true)
end
end
 
Loading
Loading
@@ -53,12 +59,15 @@ RSpec.describe Resolvers::Snippets::BlobsResolver do
expect(resolve_blobs(snippet, paths: paths)).to match_array(paths.map do |file|
have_attributes(path: file)
end)
expect(query_context[:unretrievable_blobs?]).to eq(false)
end
end
end
end
 
def resolve_blobs(snippet, user: current_user, paths: [], args: { paths: paths })
resolve(described_class, args: args, ctx: { current_user: user }, obj: snippet)
def resolve_blobs(snippet, user: current_user, paths: [], args: { paths: paths }, has_unretrievable_blobs: false)
query_context[:current_user] = user
query_context[:unretrievable_blobs?] = has_unretrievable_blobs
resolve(described_class, args: args, ctx: query_context, obj: snippet)
end
end
Loading
Loading
@@ -3,6 +3,8 @@
require 'spec_helper'
 
RSpec.describe Snippet do
include FakeBlobHelpers
describe 'modules' do
subject { described_class }
 
Loading
Loading
@@ -526,6 +528,21 @@ RSpec.describe Snippet do
end
end
 
describe '#all_files' do
let(:snippet) { create(:snippet, :repository) }
let(:files) { double(:files) }
subject(:all_files) { snippet.all_files }
before do
allow(snippet.repository).to receive(:ls_files).with(snippet.default_branch).and_return(files)
end
it 'lists files from the repository with the default branch' do
expect(all_files).to eq(files)
end
end
describe '#blobs' do
context 'when repository does not exist' do
let(:snippet) { create(:snippet) }
Loading
Loading
@@ -552,6 +569,23 @@ RSpec.describe Snippet do
end
end
end
context 'when some blobs are not retrievable from repository' do
let(:snippet) { create(:snippet, :repository) }
let(:container) { double(:container) }
let(:retrievable_filename) { 'retrievable_file'}
let(:unretrievable_filename) { 'unretrievable_file'}
before do
allow(snippet).to receive(:list_files).and_return([retrievable_filename, unretrievable_filename])
blob = fake_blob(path: retrievable_filename, container: container)
allow(snippet.repository).to receive(:blobs_at).and_return([blob, nil])
end
it 'does not include unretrievable blobs' do
expect(snippet.blobs.map(&:name)).to contain_exactly(retrievable_filename)
end
end
end
 
describe '#to_json' do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment