Skip to content

Set content-type as plain/text for raw artifact endpoint

Max Orefice requested to merge morefice/fix-artifacts-content-type-endpoint into master

Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/214707

What does this MR do and why?

Following up !83515 (merged) & !83917 (merged)

This MR is a second attempt to set let workhorse setting content-type as text/plainonly for our raw endpoint when serving artifacts.

Let workhorse set content-type endpoints
:id/jobs/:job_id/artifacts/*artifact_path
:id/jobs/artifacts/:ref_name/raw/*artifact_path

It lets workhorse sets the response header itself as decided with the team (this is the same technique used for git blobs) which default content type to text/plain.

Why are we doing this?

When fetching artifact using ArtifactsController#raw method they are served with their "real" MIME type in the Content-Type headers. This allows an attacker to host a malicious JavaScript payload as an artifact and bypass our CSP.

Testing locally

  1. Create a job which generate an artifact (in this example I've used a javascript file)
  2. Here a snippet of the artifact I've created in my .gitlatb-ci.yml
test-workhorse-header:
  script:
    - echo "alert('hello!');" > hello.js
  artifacts:
    paths:
      - hello.js
  1. Verify the headers of the response with curl
$ curl --head -i 127.0.0.1:3000/root/<PROJECT_ID>/-/jobs/<JOB_ID>/artifacts/raw/hello.js
  1. Verify the headers for our public API response with curl remains the same
$ curl --head -i 127.0.0.1:3000/api/v4/projects/<PROJECT_ID>/jobs/<JOB_ID>/artifacts/hello.js

Screenshots

Before this MR

Content-Type application/javascript
➜  gitlab git:(master) ✗ curl --head -i http://127.0.0.1:3000/root/test-workhorse-header/-/jobs/769/artifacts/raw/hello.js
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Disposition: attachment; filename="hello.js"
Content-Length: 17
Content-Security-Policy: base-uri 'self'; child-src https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com https://www.googletagmanager.com/ns.html http://127.0.0.1:3000/rails/letter_opener/ http://127.0.0.1:3000/admin/ http://127.0.0.1:3000/assets/ http://127.0.0.1:3000/-/speedscope/index.html http://127.0.0.1:3000/-/sandbox/mermaid http://127.0.0.1:3000/assets/ blob: data:; connect-src 'self' http://127.0.0.1:3808 ws://127.0.0.1:3808 ws://127.0.0.1:3000; default-src 'self'; font-src 'self'; form-action 'self' https: http:; frame-ancestors 'self'; frame-src https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com https://www.googletagmanager.com/ns.html http://127.0.0.1:3000/rails/letter_opener/ http://127.0.0.1:3000/admin/ http://127.0.0.1:3000/assets/ http://127.0.0.1:3000/-/speedscope/index.html http://127.0.0.1:3000/-/sandbox/mermaid; img-src 'self' data: blob: http: https:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com 'nonce-V95XI2Lo/57qhXOoL9VXLg=='; style-src 'self' 'unsafe-inline'; worker-src http://127.0.0.1:3000/assets/ blob: data:
- Content-Type: application/javascript
Permissions-Policy: interest-cohort=()
Pragma: no-cache
Referrer-Policy: strict-origin-when-cross-origin
Set-Cookie: perf_bar_enabled=true; path=/
X-Accel-Buffering: no
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 01FY9FF1M0VS8BD3Q6T5PYW8W2
X-Runtime: 0.388220
X-Ua-Compatible: IE=edge
X-Xss-Protection: 1; mode=block
Date: Wed, 16 Mar 2022 13:42:50 GMT

After this MR

Content-Type text/plain
➜  gitlab git:(master) ✗ curl --head -i http://127.0.0.1:3000/root/test-workhorse-header/-/jobs/769/artifacts/raw/hello.js
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Disposition: attachment; filename="hello.js"
Content-Length: 17
Content-Security-Policy: base-uri 'self'; child-src https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com https://www.googletagmanager.com/ns.html http://127.0.0.1:3000/rails/letter_opener/ http://127.0.0.1:3000/admin/ http://127.0.0.1:3000/assets/ http://127.0.0.1:3000/-/speedscope/index.html http://127.0.0.1:3000/-/sandbox/mermaid http://127.0.0.1:3000/assets/ blob: data:; connect-src 'self' http://127.0.0.1:3808 ws://127.0.0.1:3808 ws://127.0.0.1:3000; default-src 'self'; font-src 'self'; form-action 'self' https: http:; frame-ancestors 'self'; frame-src https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com https://www.googletagmanager.com/ns.html http://127.0.0.1:3000/rails/letter_opener/ http://127.0.0.1:3000/admin/ http://127.0.0.1:3000/assets/ http://127.0.0.1:3000/-/speedscope/index.html http://127.0.0.1:3000/-/sandbox/mermaid; img-src 'self' data: blob: http: https:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com 'nonce-jbH65kov3LsrnoNS0blojQ=='; style-src 'self' 'unsafe-inline'; worker-src http://127.0.0.1:3000/assets/ blob: data:
+ Content-Type: text/plain; charset=utf-8
Permissions-Policy: interest-cohort=()
Pragma: no-cache
Referrer-Policy: strict-origin-when-cross-origin
Server: thin
Set-Cookie: perf_bar_enabled=true; path=/
X-Accel-Buffering: no
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 01FY9FJ7SVW03Q1GVZ7NGBSQ5W
X-Runtime: 6.694900
X-Ua-Compatible: IE=edge
X-Xss-Protection: 1; mode=block
Date: Wed, 16 Mar 2022 13:44:41 GMT

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Max Orefice

Merge request reports