Commit 6e1f0852 authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'security-osw-stop-linking-to-packages' into 'master'

Stop linking to unrecognized package sources

See merge request gitlab/gitlabhq!2933
parents 6c3482d1 4537fbb8
...@@ -3,9 +3,4 @@ ...@@ -3,9 +3,4 @@
This project manages its dependencies using This project manages its dependencies using
%strong= viewer.manager_name %strong= viewer.manager_name
- if viewer.package_name
and defines a #{viewer.package_type} named
%strong<
= link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer' = link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
---
title: Stop linking to unrecognized package sources
merge_request: 55518
author:
type: security
...@@ -4,6 +4,7 @@ module Gitlab ...@@ -4,6 +4,7 @@ module Gitlab
module DependencyLinker module DependencyLinker
class BaseLinker class BaseLinker
URL_REGEX = %r{https?://[^'" ]+}.freeze URL_REGEX = %r{https?://[^'" ]+}.freeze
GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze
REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
class_attribute :file_type class_attribute :file_type
...@@ -29,8 +30,25 @@ module Gitlab ...@@ -29,8 +30,25 @@ module Gitlab
highlighted_lines.join.html_safe highlighted_lines.join.html_safe
end end
def external_url(name, external_ref)
return if external_ref =~ GIT_INVALID_URL_REGEX
case external_ref
when /\A#{URL_REGEX}\z/
external_ref
when /\A#{REPO_REGEX}\z/
github_url(external_ref)
else
package_url(name)
end
end
private private
def package_url(_name)
raise NotImplementedError
end
def link_dependencies def link_dependencies
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -8,8 +8,8 @@ module Gitlab ...@@ -8,8 +8,8 @@ module Gitlab
private private
def link_packages def link_packages
link_packages_at_key("require", &method(:package_url)) link_packages_at_key("require")
link_packages_at_key("require-dev", &method(:package_url)) link_packages_at_key("require-dev")
end end
def package_url(name) def package_url(name)
......
...@@ -3,8 +3,14 @@ ...@@ -3,8 +3,14 @@
module Gitlab module Gitlab
module DependencyLinker module DependencyLinker
class GemfileLinker < MethodLinker class GemfileLinker < MethodLinker
class_attribute :package_keyword
self.package_keyword = :gem
self.file_type = :gemfile self.file_type = :gemfile
GITHUB_REGEX = /(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/.freeze
GIT_REGEX = /(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/.freeze
private private
def link_dependencies def link_dependencies
...@@ -14,21 +20,35 @@ module Gitlab ...@@ -14,21 +20,35 @@ module Gitlab
def link_urls def link_urls
# Link `github: "user/repo"` to https://github.com/user/repo # Link `github: "user/repo"` to https://github.com/user/repo
link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/, &method(:github_url)) link_regex(GITHUB_REGEX, &method(:github_url))
# Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo # Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo
link_regex(/(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/, &:itself) link_regex(GIT_REGEX, &:itself)
# Link `source "https://rubygems.org"` to https://rubygems.org # Link `source "https://rubygems.org"` to https://rubygems.org
link_method_call('source', URL_REGEX, &:itself) link_method_call('source', URL_REGEX, &:itself)
end end
def link_packages def link_packages
# Link `gem "package_name"` to https://rubygems.org/gems/package_name packages = parse_packages
link_method_call('gem') do |name|
"https://rubygems.org/gems/#{name}" return if packages.blank?
packages.each do |package|
link_method_call('gem', package.name) do
external_url(package.name, package.external_ref)
end
end end
end end
def package_url(name)
"https://rubygems.org/gems/#{name}"
end
def parse_packages
parser = Gitlab::DependencyLinker::Parser::Gemfile.new(plain_text)
parser.parse(keyword: self.class.package_keyword)
end
end end
end end
end end
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
link_method_call('homepage', URL_REGEX, &:itself) link_method_call('homepage', URL_REGEX, &:itself)
link_method_call('license', &method(:license_url)) link_method_call('license', &method(:license_url))
link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) do |name| link_method_call(%w[add_dependency add_runtime_dependency add_development_dependency]) do |name|
"https://rubygems.org/gems/#{name}" "https://rubygems.org/gems/#{name}"
end end
end end
......
...@@ -23,18 +23,22 @@ module Gitlab ...@@ -23,18 +23,22 @@ module Gitlab
# link_method_call('name') # link_method_call('name')
# # Will link `package` in `self.name = "package"` # # Will link `package` in `self.name = "package"`
def link_method_call(method_name, value = nil, &url_proc) def link_method_call(method_name, value = nil, &url_proc)
regex = method_call_regex(method_name, value)
link_regex(regex, &url_proc)
end
def method_call_regex(method_name, value = nil)
method_name = regexp_for_value(method_name) method_name = regexp_for_value(method_name)
value = regexp_for_value(value) value = regexp_for_value(value)
regex = %r{ %r{
#{method_name} # Method name #{method_name} # Method name
\s* # Whitespace \s* # Whitespace
[(=]? # Opening brace or equals sign [(=]? # Opening brace or equals sign
\s* # Whitespace \s* # Whitespace
['"](?<name>#{value})['"] # Package name in quotes ['"](?<name>#{value})['"] # Package name in quotes
}x }x
link_regex(regex, &url_proc)
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module DependencyLinker
class Package
attr_reader :name, :git_ref, :github_ref
def initialize(name, git_ref, github_ref)
@name = name
@git_ref = git_ref
@github_ref = github_ref
end
def external_ref
@git_ref || @github_ref
end
end
end
end
...@@ -8,7 +8,6 @@ module Gitlab ...@@ -8,7 +8,6 @@ module Gitlab
private private
def link_dependencies def link_dependencies
link_json('name', json["name"], &method(:package_url))
link_json('license', &method(:license_url)) link_json('license', &method(:license_url))
link_json(%w[homepage url], URL_REGEX, &:itself) link_json(%w[homepage url], URL_REGEX, &:itself)
...@@ -16,25 +15,19 @@ module Gitlab ...@@ -16,25 +15,19 @@ module Gitlab
end end
def link_packages def link_packages
link_packages_at_key("dependencies", &method(:package_url)) link_packages_at_key("dependencies")
link_packages_at_key("devDependencies", &method(:package_url)) link_packages_at_key("devDependencies")
end end
def link_packages_at_key(key, &url_proc) def link_packages_at_key(key)
dependencies = json[key] dependencies = json[key]
return unless dependencies return unless dependencies
dependencies.each do |name, version| dependencies.each do |name, version|
link_json(name, version, link: :key, &url_proc) external_url = external_url(name, version)
link_json(name) do |value| link_json(name, version, link: :key) { external_url }
case value link_json(name) { external_url }
when /\A#{URL_REGEX}\z/
value
when /\A#{REPO_REGEX}\z/
github_url(value)
end
end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module DependencyLinker
module Parser
class Gemfile < MethodLinker
GIT_REGEX = Gitlab::DependencyLinker::GemfileLinker::GIT_REGEX
GITHUB_REGEX = Gitlab::DependencyLinker::GemfileLinker::GITHUB_REGEX
def initialize(plain_text)
@plain_text = plain_text
end
# Returns a list of Gitlab::DependencyLinker::Package
#
# keyword - The package definition keyword, e.g. `:gem` for
# Gemfile parsing, `:pod` for Podfile.
def parse(keyword:)
plain_lines.each_with_object([]) do |line, packages|
name = fetch(line, method_call_regex(keyword))
next unless name
git_ref = fetch(line, GIT_REGEX)
github_ref = fetch(line, GITHUB_REGEX)
packages << Gitlab::DependencyLinker::Package.new(name, git_ref, github_ref)
end
end
private
def fetch(line, regex, group: :name)
match = line.match(regex)
match[group] if match
end
end
end
end
end
...@@ -5,12 +5,21 @@ module Gitlab ...@@ -5,12 +5,21 @@ module Gitlab
class PodfileLinker < GemfileLinker class PodfileLinker < GemfileLinker
include Cocoapods include Cocoapods
self.package_keyword = :pod
self.file_type = :podfile self.file_type = :podfile
private private
def link_packages def link_packages
link_method_call('pod', &method(:package_url)) packages = parse_packages
return unless packages
packages.each do |package|
link_method_call('pod', package.name) do
external_url(package.name, package.external_ref)
end
end
end end
end end
end end
......
...@@ -19,7 +19,7 @@ module Gitlab ...@@ -19,7 +19,7 @@ module Gitlab
link_method_call('license', &method(:license_url)) link_method_call('license', &method(:license_url))
link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url)) link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url))
link_method_call(%w[name dependency], &method(:package_url)) link_method_call('dependency', &method(:package_url))
end end
end end
end end
......
...@@ -548,10 +548,7 @@ describe 'File blob', :js do ...@@ -548,10 +548,7 @@ describe 'File blob', :js do
it 'displays an auxiliary viewer' do it 'displays an auxiliary viewer' do
aggregate_failures do aggregate_failures do
# shows names of dependency manager and package # shows names of dependency manager and package
expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.') expect(page).to have_content('This project manages its dependencies using RubyGems.')
# shows a link to the gem
expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord')
# shows a learn more link # shows a learn more link
expect(page).to have_link('Learn more', href: 'https://rubygems.org/') expect(page).to have_link('Learn more', href: 'https://rubygems.org/')
......
...@@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do ...@@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the module name' do it 'does not link the module name' do
expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel'))
end end
it 'links the homepage' do it 'links the homepage' do
......
...@@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do ...@@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do
end end
it 'links dependencies' do it 'links dependencies' do
expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails'))
expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer')) expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer'))
expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders'))
expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets'))
expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for')) expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for'))
end end
it 'links to external dependencies' do
expect(subject).to include(link('rails', 'https://github.com/rails/rails'))
expect(subject).to include(link('responders', 'https://github.com/rails/responders'))
expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets'))
end
it 'links GitHub repos' do it 'links GitHub repos' do
expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails')) expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails'))
expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders')) expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders'))
......
...@@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do ...@@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the gem name' do it 'does not link the gem name' do
expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git'))
end end
it 'links the license' do it 'links the license' do
......
...@@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do ...@@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
"express": "4.2.x", "express": "4.2.x",
"bigpipe": "bigpipe/pagelet", "bigpipe": "bigpipe/pagelet",
"plates": "https://github.com/flatiron/plates/tarball/master", "plates": "https://github.com/flatiron/plates/tarball/master",
"karma": "^1.4.1" "karma": "^1.4.1",
"random": "git+https://EdOverflow@github.com/example/example.git"
}, },
"devDependencies": { "devDependencies": {
"vows": "^0.7.0", "vows": "^0.7.0",
...@@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do ...@@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the module name' do it 'does not link the module name' do
expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name')) expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name'))
end end
it 'links the homepage' do it 'links the homepage' do
...@@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do ...@@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
expect(subject).to include(link('primus', 'https://npmjs.com/package/primus')) expect(subject).to include(link('primus', 'https://npmjs.com/package/primus'))
expect(subject).to include(link('async', 'https://npmjs.com/package/async')) expect(subject).to include(link('async', 'https://npmjs.com/package/async'))
expect(subject).to include(link('express', 'https://npmjs.com/package/express')) expect(subject).to include(link('express', 'https://npmjs.com/package/express'))
expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe'))
expect(subject).to include(link('plates', 'https://npmjs.com/package/plates'))
expect(subject).to include(link('karma', 'https://npmjs.com/package/karma')) expect(subject).to include(link('karma', 'https://npmjs.com/package/karma'))
expect(subject).to include(link('vows', 'https://npmjs.com/package/vows')) expect(subject).to include(link('vows', 'https://npmjs.com/package/vows'))
expect(subject).to include(link('assume', 'https://npmjs.com/package/assume')) expect(subject).to include(link('assume', 'https://npmjs.com/package/assume'))
expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit')) expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit'))
end end
it 'links dependencies to URL detected on value' do
expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet'))
expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master'))
end
it 'does not link to NPM when invalid git URL' do
expect(subject).not_to include(link('random', 'https://npmjs.com/package/random'))
end
it 'links GitHub repos' do it 'links GitHub repos' do
expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet')) expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet'))
end end
......
require 'rails_helper'
describe Gitlab::DependencyLinker::Parser::Gemfile do
describe '#parse' do
let(:file_content) do
<<-CONTENT.strip_heredoc
source 'https://rubygems.org'
gem "rails", '4.2.6', github: "rails/rails"
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'responders', '~> 2.0', :github => 'rails/responders'
gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets'
gem 'default_value_for', '~> 3.0.0'
CONTENT
end
subject { described_class.new(file_content).parse(keyword: 'gem') }
def fetch_package(name)
subject.find { |package| package.name == name }
end
it 'returns parsed packages' do
expect(subject.size).to eq(5)
expect(subject).to all(be_a(Gitlab::DependencyLinker::Package))
end
it 'packages respond to name and external_ref accordingly' do
expect(fetch_package('rails')).to have_attributes(name: 'rails',
github_ref: 'rails/rails',
git_ref: nil)
expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets',
github_ref: nil,
git_ref: 'https://gitlab.example.com/gems/sprockets')
expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for',
github_ref: nil,
git_ref: nil)
end
end
end
...@@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do ...@@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do
it 'links packages' do it 'links packages' do
expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking'))
expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar')) end
it 'links external packages' do
expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git'))
end end
it 'links Git repos' do it 'links Git repos' do
......
...@@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do ...@@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the gem name' do it 'does not link the pod name' do
expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability'))
end end
it 'links the license' do it 'links the license' do
......
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