Commit 23da85f7 authored by Alessio Caiazza's avatar Alessio Caiazza

Merge branch 'security/gitlab-issue-213139' into 'master'

Sign artifact multipart fields in Workhorse

See merge request gitlab-org/security/gitlab-workhorse!7
parents 1dc5c363 28ba9f6a
# Changelog for gitlab-workhorse # Changelog for gitlab-workhorse
v 8.30.1
- Sign artifact multipart fields in Workhorse
v 8.30.0 v 8.30.0
- Proxy ActionCable websocket connection !454 - Proxy ActionCable websocket connection !454
...@@ -21,6 +25,14 @@ v 8.26.0 ...@@ -21,6 +25,14 @@ v 8.26.0
- Add route for project imports direct upload via UI !470 - Add route for project imports direct upload via UI !470
v 8.25.2
- Sign artifact multipart fields in Workhorse
v 8.25.1
- Reject parameters that override upload fields
v 8.25.0 v 8.25.0
- Add route for project imports direct upload !459 - Add route for project imports direct upload !459
...@@ -38,10 +50,26 @@ v 8.22.0 ...@@ -38,10 +50,26 @@ v 8.22.0
- Bump the version of golang.org/x/sys !456 - Bump the version of golang.org/x/sys !456
- Add friendly development error page for 502 !453 - Add friendly development error page for 502 !453
v 8.21.2
- Sign artifact multipart fields in Workhorse
v 8.21.1
- Reject parameters that override upload fields
v 8.21.0 v 8.21.0
- Add route for group imports direct upload !455 - Add route for group imports direct upload !455
v 8.20.2
- Sign artifact multipart fields in Workhorse
v 8.20.1
- Reject parameters that override upload fields
v 8.20.0 v 8.20.0
- Sign file upload requests modified by workhorse - Sign file upload requests modified by workhorse
......
...@@ -19,7 +19,8 @@ import ( ...@@ -19,7 +19,8 @@ import (
type artifactsUploadProcessor struct { type artifactsUploadProcessor struct {
opts *filestore.SaveFileOpts opts *filestore.SaveFileOpts
stored bool
upload.SavedFileTracker
} }
func (a *artifactsUploadProcessor) generateMetadataFromZip(ctx context.Context, file *filestore.FileHandler) (*filestore.FileHandler, error) { func (a *artifactsUploadProcessor) generateMetadataFromZip(ctx context.Context, file *filestore.FileHandler) (*filestore.FileHandler, error) {
...@@ -79,10 +80,10 @@ func (a *artifactsUploadProcessor) ProcessFile(ctx context.Context, formName str ...@@ -79,10 +80,10 @@ func (a *artifactsUploadProcessor) ProcessFile(ctx context.Context, formName str
if formName != "file" { if formName != "file" {
return fmt.Errorf("invalid form field: %q", formName) return fmt.Errorf("invalid form field: %q", formName)
} }
if a.stored { if a.Count() > 0 {
return fmt.Errorf("artifacts request contains more than one file") return fmt.Errorf("artifacts request contains more than one file")
} }
a.stored = true a.Track(formName, file.LocalPath)
select { select {
case <-ctx.Done(): case <-ctx.Done():
...@@ -99,26 +100,20 @@ func (a *artifactsUploadProcessor) ProcessFile(ctx context.Context, formName str ...@@ -99,26 +100,20 @@ func (a *artifactsUploadProcessor) ProcessFile(ctx context.Context, formName str
for k, v := range metadata.GitLabFinalizeFields("metadata") { for k, v := range metadata.GitLabFinalizeFields("metadata") {
writer.WriteField(k, v) writer.WriteField(k, v)
} }
a.Track("metadata", metadata.LocalPath)
} }
} }
return nil return nil
} }
func (a *artifactsUploadProcessor) ProcessField(ctx context.Context, formName string, writer *multipart.Writer) error {
return nil
}
func (a *artifactsUploadProcessor) Finalize(ctx context.Context) error {
return nil
}
func (a *artifactsUploadProcessor) Name() string { func (a *artifactsUploadProcessor) Name() string {
return "artifacts" return "artifacts"
} }
func UploadArtifacts(myAPI *api.API, h http.Handler) http.Handler { func UploadArtifacts(myAPI *api.API, h http.Handler) http.Handler {
return myAPI.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) { return myAPI.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
mg := &artifactsUploadProcessor{opts: filestore.GetOpts(a)} mg := &artifactsUploadProcessor{opts: filestore.GetOpts(a), SavedFileTracker: upload.SavedFileTracker{Request: r}}
upload.HandleFileUploads(w, r, h, a, mg) upload.HandleFileUploads(w, r, h, a, mg)
}, "/authorize") }, "/authorize")
......
...@@ -14,13 +14,18 @@ import ( ...@@ -14,13 +14,18 @@ import (
"os" "os"
"testing" "testing"
jwt "github.com/dgrijalva/jwt-go"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore" "gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper" "gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/proxy" "gitlab.com/gitlab-org/gitlab-workhorse/internal/proxy"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper" "gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/upload"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream/roundtripper" "gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream/roundtripper"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/zipartifacts" "gitlab.com/gitlab-org/gitlab-workhorse/internal/zipartifacts"
"github.com/stretchr/testify/require"
) )
const ( const (
...@@ -128,7 +133,18 @@ func TestUploadHandlerAddingMetadata(t *testing.T) { ...@@ -128,7 +133,18 @@ func TestUploadHandlerAddingMetadata(t *testing.T) {
} }
defer os.RemoveAll(tempPath) defer os.RemoveAll(tempPath)
ts := testArtifactsUploadServer(t, api.Response{TempPath: tempPath}, nil) ts := testArtifactsUploadServer(t, api.Response{TempPath: tempPath},
func(w http.ResponseWriter, r *http.Request) {
jwtToken, err := jwt.Parse(r.Header.Get(upload.RewrittenFieldsHeader), testhelper.ParseJWT)
require.NoError(t, err)
rewrittenFields := jwtToken.Claims.(jwt.MapClaims)["rewritten_fields"].(map[string]interface{})
require.Equal(t, 2, len(rewrittenFields))
require.Contains(t, rewrittenFields, "file")
require.Contains(t, rewrittenFields, "metadata")
},
)
defer ts.Close() defer ts.Close()
var buffer bytes.Buffer var buffer bytes.Buffer
......
...@@ -15,6 +15,8 @@ import ( ...@@ -15,6 +15,8 @@ import (
"strings" "strings"
"testing" "testing"
jwt "github.com/dgrijalva/jwt-go"
"gitlab.com/gitlab-org/labkit/log" "gitlab.com/gitlab-org/labkit/log"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/secret" "gitlab.com/gitlab-org/gitlab-workhorse/internal/secret"
...@@ -174,3 +176,18 @@ func LoadFile(t *testing.T, filePath string) string { ...@@ -174,3 +176,18 @@ func LoadFile(t *testing.T, filePath string) string {
} }
return string(content) return string(content)
} }
func ParseJWT(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
ConfigureSecret()
secretBytes, err := secret.Bytes()
if err != nil {
return nil, fmt.Errorf("read secret from file: %v", err)
}
return secretBytes, nil
}
package upload package upload
import ( import (
"context"
"fmt"
"mime/multipart"
"net/http" "net/http"
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore" "gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/secret"
) )
const RewrittenFieldsHeader = "Gitlab-Workhorse-Multipart-Fields" const RewrittenFieldsHeader = "Gitlab-Workhorse-Multipart-Fields"
type savedFileTracker struct {
request *http.Request
rewrittenFields map[string]string
}
type MultipartClaims struct { type MultipartClaims struct {
RewrittenFields map[string]string `json:"rewritten_fields"` RewrittenFields map[string]string `json:"rewritten_fields"`
jwt.StandardClaims jwt.StandardClaims
...@@ -27,38 +18,7 @@ type MultipartClaims struct { ...@@ -27,38 +18,7 @@ type MultipartClaims struct {
func Accelerate(rails filestore.PreAuthorizer, h http.Handler) http.Handler { func Accelerate(rails filestore.PreAuthorizer, h http.Handler) http.Handler {
return rails.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) { return rails.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
s := &savedFileTracker{request: r} s := &SavedFileTracker{Request: r}
HandleFileUploads(w, r, h, a, s) HandleFileUploads(w, r, h, a, s)
}, "/authorize") }, "/authorize")
} }
func (s *savedFileTracker) ProcessFile(_ context.Context, fieldName string, file *filestore.FileHandler, _ *multipart.Writer) error {
if s.rewrittenFields == nil {
s.rewrittenFields = make(map[string]string)
}
s.rewrittenFields[fieldName] = file.LocalPath
return nil
}
func (s *savedFileTracker) ProcessField(_ context.Context, _ string, _ *multipart.Writer) error {
return nil
}
func (s *savedFileTracker) Finalize(_ context.Context) error {
if s.rewrittenFields == nil {
return nil
}
claims := MultipartClaims{s.rewrittenFields, secret.DefaultClaims}
tokenString, err := secret.JWTTokenString(claims)
if err != nil {
return fmt.Errorf("savedFileTracker.Finalize: %v", err)
}
s.request.Header.Set(RewrittenFieldsHeader, tokenString)
return nil
}
func (a *savedFileTracker) Name() string {
return "accelerate"
}
package upload
import (
"context"
"fmt"
"mime/multipart"
"net/http"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/secret"
)
type SavedFileTracker struct {
Request *http.Request
rewrittenFields map[string]string
}
func (s *SavedFileTracker) Track(fieldName string, localPath string) {
if s.rewrittenFields == nil {
s.rewrittenFields = make(map[string]string)
}
s.rewrittenFields[fieldName] = localPath
}
func (s *SavedFileTracker) Count() int {
return len(s.rewrittenFields)
}
func (s *SavedFileTracker) ProcessFile(_ context.Context, fieldName string, file *filestore.FileHandler, _ *multipart.Writer) error {
s.Track(fieldName, file.LocalPath)
return nil
}
func (s *SavedFileTracker) ProcessField(_ context.Context, _ string, _ *multipart.Writer) error {
return nil
}
func (s *SavedFileTracker) Finalize(_ context.Context) error {
if s.rewrittenFields == nil {
return nil
}
claims := MultipartClaims{RewrittenFields: s.rewrittenFields, StandardClaims: secret.DefaultClaims}
tokenString, err := secret.JWTTokenString(claims)
if err != nil {
return fmt.Errorf("savedFileTracker.Finalize: %v", err)
}
s.Request.Header.Set(RewrittenFieldsHeader, tokenString)
return nil
}
func (s *SavedFileTracker) Name() string {
return "accelerate"
}
package upload
import (
"context"
jwt "github.com/dgrijalva/jwt-go"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
)
func TestSavedFileTracking(t *testing.T) {
testhelper.ConfigureSecret()
r, err := http.NewRequest("PUT", "/url/path", nil)
require.NoError(t, err)
tracker := SavedFileTracker{Request: r}
require.Equal(t, "accelerate", tracker.Name())
file := &filestore.FileHandler{}
ctx := context.Background()
tracker.ProcessFile(ctx, "test", file, nil)
require.Equal(t, 1, tracker.Count())
tracker.Finalize(ctx)
jwtToken, err := jwt.Parse(r.Header.Get(RewrittenFieldsHeader), testhelper.ParseJWT)
require.NoError(t, err)
rewrittenFields := jwtToken.Claims.(jwt.MapClaims)["rewritten_fields"].(map[string]interface{})
require.Equal(t, 1, len(rewrittenFields))
require.Contains(t, rewrittenFields, "test")
}
...@@ -390,7 +390,7 @@ func TestInvalidFileNames(t *testing.T) { ...@@ -390,7 +390,7 @@ func TestInvalidFileNames(t *testing.T) {
httpRequest.Header.Set("Content-Type", writer.FormDataContentType()) httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder() response := httptest.NewRecorder()
HandleFileUploads(response, httpRequest, nilHandler, &api.Response{TempPath: tempPath}, &savedFileTracker{request: httpRequest}) HandleFileUploads(response, httpRequest, nilHandler, &api.Response{TempPath: tempPath}, &SavedFileTracker{Request: httpRequest})
testhelper.AssertResponseCode(t, response, testCase.code) testhelper.AssertResponseCode(t, response, testCase.code)
} }
} }
......
...@@ -63,7 +63,7 @@ func TestArtifactsUpload(t *testing.T) { ...@@ -63,7 +63,7 @@ func TestArtifactsUpload(t *testing.T) {
func expectSignedRequest(t *testing.T, r *http.Request) { func expectSignedRequest(t *testing.T, r *http.Request) {
t.Helper() t.Helper()
_, err := jwt.Parse(r.Header.Get(secret.RequestHeader), parseJWT) _, err := jwt.Parse(r.Header.Get(secret.RequestHeader), testhelper.ParseJWT)
require.NoError(t, err) require.NoError(t, err)
} }
...@@ -109,21 +109,6 @@ func signedUploadTestServer(t *testing.T, extraTests func(r *http.Request)) *htt ...@@ -109,21 +109,6 @@ func signedUploadTestServer(t *testing.T, extraTests func(r *http.Request)) *htt
}) })
} }
func parseJWT(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
testhelper.ConfigureSecret()
secretBytes, err := secret.Bytes()
if err != nil {
return nil, fmt.Errorf("read secret from file: %v", err)
}
return secretBytes, nil
}
func TestAcceleratedUpload(t *testing.T) { func TestAcceleratedUpload(t *testing.T) {
tests := []struct { tests := []struct {
method string method string
...@@ -150,7 +135,7 @@ func TestAcceleratedUpload(t *testing.T) { ...@@ -150,7 +135,7 @@ func TestAcceleratedUpload(t *testing.T) {
expectSignedRequest(t, r) expectSignedRequest(t, r)
} }
jwtToken, err := jwt.Parse(r.Header.Get(upload.RewrittenFieldsHeader), parseJWT) jwtToken, err := jwt.Parse(r.Header.Get(upload.RewrittenFieldsHeader), testhelper.ParseJWT)
require.NoError(t, err) require.NoError(t, err)
rewrittenFields := jwtToken.Claims.(jwt.MapClaims)["rewritten_fields"].(map[string]interface{}) rewrittenFields := jwtToken.Claims.(jwt.MapClaims)["rewritten_fields"].(map[string]interface{})
......
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