Commit d9693964 authored by Kerri Miller's avatar Kerri Miller

Disable caching on API project/raw endpoint

Caching of file contents creates an inconsistency in the value of the
Content-Disposition header, allowing files that should only be sent as
"attachment" to instead be returned as "inline," causing them to be
evaluated and executed by the receiving client. This is due to how
gitaly and the main Rails application coordinate around evaluating etags
for content freshness. This fix addresses the issue by removing caching
from this endpoint, but does not address the underlying issue (namely
that Rails can not accurately determine the file type of the requested
content, thus can not be responsible for determining appropriate or safe
Content-Disposition.)
parent f1ce70ab
...@@ -5,6 +5,7 @@ require 'fogbugz' ...@@ -5,6 +5,7 @@ require 'fogbugz'
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Gitlab::GonHelper include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper include GitlabRoutingHelper
include PageLayoutHelper include PageLayoutHelper
include SafeParamsHelper include SafeParamsHelper
...@@ -55,7 +56,6 @@ class ApplicationController < ActionController::Base ...@@ -55,7 +56,6 @@ class ApplicationController < ActionController::Base
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
# concerns due to caching private data. # concerns due to caching private data.
DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store" DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store"
DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache"
rescue_from Encoding::CompatibilityError do |exception| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
...@@ -247,9 +247,9 @@ class ApplicationController < ActionController::Base ...@@ -247,9 +247,9 @@ class ApplicationController < ActionController::Base
end end
def no_cache_headers def no_cache_headers
headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v|
headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility headers[k] = v
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' end
end end
def default_headers def default_headers
......
---
title: Disable caching of repository/files/:file_path/raw API endpoint
merge_request:
author:
type: security
...@@ -127,6 +127,7 @@ module API ...@@ -127,6 +127,7 @@ module API
get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do
assign_file_vars! assign_file_vars!
no_cache_headers
set_http_headers(blob_data) set_http_headers(blob_data)
send_git_blob @repo, @blob send_git_blob @repo, @blob
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module API module API
module Helpers module Helpers
module HeadersHelpers module HeadersHelpers
include Gitlab::NoCacheHeaders
def set_http_headers(header_data) def set_http_headers(header_data)
header_data.each do |key, value| header_data.each do |key, value|
if value.is_a?(Enumerable) if value.is_a?(Enumerable)
...@@ -12,6 +14,12 @@ module API ...@@ -12,6 +14,12 @@ module API
header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value.to_s header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value.to_s
end end
end end
def no_cache_headers
DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v|
header k, v
end
end
end end
end end
end end
# frozen_string_literal: true
module Gitlab
module NoCacheHeaders
DEFAULT_GITLAB_NO_CACHE_HEADERS = {
'Cache-Control' => "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store, no-cache",
'Pragma' => 'no-cache', # HTTP 1.0 compatibility
'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT'
}.freeze
def no_cache_headers
raise "#no_cache_headers is not implemented for this object"
end
end
end
...@@ -59,5 +59,48 @@ module QA ...@@ -59,5 +59,48 @@ module QA
a_hash_including(message: '202 Accepted') a_hash_including(message: '202 Accepted')
) )
end end
describe 'raw file access' do
let(:svg_file) do
<<-SVG
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
<script type="text/javascript">
alert("surprise");
</script>
</svg>
SVG
end
it 'sets no-cache headers as expected' do
create_project_request = Runtime::API::Request.new(@api_client, '/projects')
post create_project_request.url, path: project_name, name: project_name
create_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/test.svg")
post create_file_request.url, branch: 'master', content: svg_file, commit_message: 'Add test.svg'
get_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/test.svg/raw", ref: 'master')
3.times do
response = get get_file_request.url
# Subsequent responses aren't cached, so headers should match from
# request to request, especially a 200 response rather than a 304
# (indicating a cached response.) Further, :content_disposition
# should include `attachment` for all responses.
#
expect(response.headers[:cache_control]).to include("no-store")
expect(response.headers[:cache_control]).to include("no-cache")
expect(response.headers[:pragma]).to eq("no-cache")
expect(response.headers[:expires]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
expect(response.headers[:content_disposition]).to include("attachment")
expect(response.headers[:content_disposition]).not_to include("inline")
expect(response.headers[:content_type]).to include("image/svg+xml")
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::NoCacheHeaders do
class NoCacheTester
include Gitlab::NoCacheHeaders
end
describe "#no_cache_headers" do
subject { NoCacheTester.new }
it "raises a RuntimeError" do
expect { subject.no_cache_headers }.to raise_error(RuntimeError)
end
end
end
...@@ -447,6 +447,18 @@ describe API::Files do ...@@ -447,6 +447,18 @@ describe API::Files do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'sets no-cache headers' do
url = route('.gitignore') + "/raw"
expect(Gitlab::Workhorse).to receive(:send_git_blob)
get api(url, current_user), params: params
expect(response.headers["Cache-Control"]).to include("no-store")
expect(response.headers["Cache-Control"]).to include("no-cache")
expect(response.headers["Pragma"]).to eq("no-cache")
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
end
context 'when mandatory params are not given' do context 'when mandatory params are not given' do
it_behaves_like '400 response' do it_behaves_like '400 response' do
let(:request) { get api(route("any%2Ffile"), current_user) } let(:request) { get api(route("any%2Ffile"), current_user) }
......
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