359 lines
11 KiB
Diff
359 lines
11 KiB
Diff
|
diff -urN b/creds/creds.go a/creds/creds.go
|
||
|
--- b/creds/creds.go 2023-12-13 19:56:25.000000000 +0100
|
||
|
+++ a/creds/creds.go 2025-01-17 08:55:10.175959181 +0100
|
||
|
@@ -53,11 +53,20 @@
|
||
|
// as input.
|
||
|
type Creds map[string][]string
|
||
|
|
||
|
-func bufferCreds(c Creds) *bytes.Buffer {
|
||
|
+func (c Creds) buffer(protectProtocol bool) (*bytes.Buffer, error) {
|
||
|
buf := new(bytes.Buffer)
|
||
|
|
||
|
for k, v := range c {
|
||
|
for _, item := range v {
|
||
|
+ if strings.Contains(item, "\n") {
|
||
|
+ return nil, errors.Errorf(tr.Tr.Get("credential value for %s contains newline: %q", k, item))
|
||
|
+ }
|
||
|
+ if protectProtocol && strings.Contains(item, "\r") {
|
||
|
+ return nil, errors.Errorf(tr.Tr.Get("credential value for %s contains carriage return: %q\nIf this is intended, set `credential.protectProtocol=false`", k, item))
|
||
|
+ }
|
||
|
+ if strings.Contains(item, string(rune(0))) {
|
||
|
+ return nil, errors.Errorf(tr.Tr.Get("credential value for %s contains null byte: %q", k, item))
|
||
|
+ }
|
||
|
buf.Write([]byte(k))
|
||
|
buf.Write([]byte("="))
|
||
|
buf.Write([]byte(item))
|
||
|
@@ -65,7 +74,7 @@
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- return buf
|
||
|
+ return buf, nil
|
||
|
}
|
||
|
|
||
|
type CredentialHelperContext struct {
|
||
|
@@ -153,6 +162,9 @@
|
||
|
helpers = append(helpers, ctxt.askpassCredHelper)
|
||
|
}
|
||
|
}
|
||
|
+
|
||
|
+ ctxt.commandCredHelper.protectProtocol = ctxt.urlConfig.Bool("credential", rawurl, "protectProtocol", true)
|
||
|
+
|
||
|
return CredentialHelperWrapper{CredentialHelper: NewCredentialHelpers(append(helpers, ctxt.commandCredHelper)), Input: input, Url: u}
|
||
|
}
|
||
|
|
||
|
@@ -292,7 +304,8 @@
|
||
|
}
|
||
|
|
||
|
type commandCredentialHelper struct {
|
||
|
- SkipPrompt bool
|
||
|
+ SkipPrompt bool
|
||
|
+ protectProtocol bool
|
||
|
}
|
||
|
|
||
|
func (h *commandCredentialHelper) Fill(creds Creds) (Creds, error) {
|
||
|
@@ -323,7 +336,10 @@
|
||
|
if err != nil {
|
||
|
return nil, errors.New(tr.Tr.Get("failed to find `git credential %s`: %v", subcommand, err))
|
||
|
}
|
||
|
- cmd.Stdin = bufferCreds(input)
|
||
|
+ cmd.Stdin, err = input.buffer(h.protectProtocol)
|
||
|
+ if err != nil {
|
||
|
+ return nil, errors.New(tr.Tr.Get("invalid input to `git credential %s`: %v", subcommand, err))
|
||
|
+ }
|
||
|
cmd.Stdout = output
|
||
|
/*
|
||
|
There is a reason we don't read from stderr here:
|
||
|
diff -urN b/creds/creds_test.go a/creds/creds_test.go
|
||
|
--- b/creds/creds_test.go 2023-12-13 19:56:25.000000000 +0100
|
||
|
+++ a/creds/creds_test.go 2025-01-17 08:55:21.318023782 +0100
|
||
|
@@ -1,12 +1,89 @@
|
||
|
package creds
|
||
|
|
||
|
import (
|
||
|
+ "bytes"
|
||
|
"errors"
|
||
|
+ "slices"
|
||
|
+ "strings"
|
||
|
"testing"
|
||
|
|
||
|
"github.com/stretchr/testify/assert"
|
||
|
)
|
||
|
|
||
|
+func assertCredsLinesMatch(t *testing.T, expected []string, buf *bytes.Buffer) {
|
||
|
+ actual := strings.SplitAfter(buf.String(), "\n")
|
||
|
+
|
||
|
+ slices.Sort(expected)
|
||
|
+ slices.Sort(actual)
|
||
|
+
|
||
|
+ assert.Equal(t, expected, actual)
|
||
|
+}
|
||
|
+func TestCredsBufferFormat(t *testing.T) {
|
||
|
+ creds := make(Creds)
|
||
|
+
|
||
|
+ expected := []string{""}
|
||
|
+
|
||
|
+ buf, err := creds.buffer(true)
|
||
|
+ assert.NoError(t, err)
|
||
|
+ assertCredsLinesMatch(t, expected, buf)
|
||
|
+
|
||
|
+ creds["protocol"] = []string{"https"}
|
||
|
+ creds["host"] = []string{"example.com"}
|
||
|
+
|
||
|
+ expected = []string{"host=example.com\n", "protocol=https\n", ""}
|
||
|
+
|
||
|
+ buf, err = creds.buffer(true)
|
||
|
+ assert.NoError(t, err)
|
||
|
+ assertCredsLinesMatch(t, expected, buf)
|
||
|
+
|
||
|
+ creds["wwwauth[]"] = []string{"Basic realm=test", "Negotiate"}
|
||
|
+
|
||
|
+ expected = append(expected, "wwwauth[]=Basic realm=test\n", "wwwauth[]=Negotiate\n")
|
||
|
+ buf, err = creds.buffer(true)
|
||
|
+ assert.NoError(t, err)
|
||
|
+ assertCredsLinesMatch(t, expected, buf)
|
||
|
+}
|
||
|
+
|
||
|
+func TestCredsBufferProtect(t *testing.T) {
|
||
|
+ creds := make(Creds)
|
||
|
+
|
||
|
+ // Always disallow LF characters
|
||
|
+ creds["protocol"] = []string{"https"}
|
||
|
+ creds["host"] = []string{"one.example.com\nhost=two.example.com"}
|
||
|
+
|
||
|
+ buf, err := creds.buffer(false)
|
||
|
+ assert.Error(t, err)
|
||
|
+ assert.Nil(t, buf)
|
||
|
+
|
||
|
+ buf, err = creds.buffer(true)
|
||
|
+ assert.Error(t, err)
|
||
|
+ assert.Nil(t, buf)
|
||
|
+
|
||
|
+ // Disallow CR characters unless protocol protection disabled
|
||
|
+ creds["host"] = []string{"one.example.com\rhost=two.example.com"}
|
||
|
+
|
||
|
+ expected := []string{"", "protocol=https\n", "host=one.example.com\rhost=two.example.com\n"}
|
||
|
+
|
||
|
+ buf, err = creds.buffer(false)
|
||
|
+ assert.NoError(t, err)
|
||
|
+ assertCredsLinesMatch(t, expected, buf)
|
||
|
+
|
||
|
+ buf, err = creds.buffer(true)
|
||
|
+ assert.Error(t, err)
|
||
|
+ assert.Nil(t, buf)
|
||
|
+
|
||
|
+ // Always disallow null bytes
|
||
|
+ creds["host"] = []string{"one.example.com\x00host=two.example.com"}
|
||
|
+
|
||
|
+ buf, err = creds.buffer(false)
|
||
|
+ assert.Error(t, err)
|
||
|
+ assert.Nil(t, buf)
|
||
|
+
|
||
|
+ buf, err = creds.buffer(true)
|
||
|
+ assert.Error(t, err)
|
||
|
+ assert.Nil(t, buf)
|
||
|
+}
|
||
|
+
|
||
|
type testCredHelper struct {
|
||
|
fillErr error
|
||
|
approveErr error
|
||
|
diff -urN b/t/cmd/lfstest-gitserver.go a/t/cmd/lfstest-gitserver.go
|
||
|
--- b/t/cmd/lfstest-gitserver.go 2023-12-13 19:56:25.000000000 +0100
|
||
|
+++ a/t/cmd/lfstest-gitserver.go 2025-01-16 14:33:23.825991696 +0100
|
||
|
@@ -27,6 +27,7 @@
|
||
|
"net/http"
|
||
|
"net/http/httptest"
|
||
|
"net/textproto"
|
||
|
+ "net/url"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"regexp"
|
||
|
@@ -252,6 +253,7 @@
|
||
|
}
|
||
|
|
||
|
func lfsUrl(repo, oid string, redirect bool) string {
|
||
|
+ repo = url.QueryEscape(repo)
|
||
|
if redirect {
|
||
|
return server.URL + "/redirect307/objects/" + oid + "?r=" + repo
|
||
|
}
|
||
|
diff -urN b/t/t-credentials-protect.sh a/t/t-credentials-protect.sh
|
||
|
--- b/t/t-credentials-protect.sh 1970-01-01 01:00:00.000000000 +0100
|
||
|
+++ a/t/t-credentials-protect.sh 2025-01-16 14:03:23.597029590 +0100
|
||
|
@@ -0,0 +1,146 @@
|
||
|
+#!/usr/bin/env bash
|
||
|
+
|
||
|
+. "$(dirname "$0")/testlib.sh"
|
||
|
+
|
||
|
+ensure_git_version_isnt $VERSION_LOWER "2.3.0"
|
||
|
+
|
||
|
+export CREDSDIR="$REMOTEDIR/creds-credentials-protect"
|
||
|
+setup_creds
|
||
|
+
|
||
|
+# Copy the default record file for the test credential helper to match the
|
||
|
+# hostname used in the Git LFS configurations of the tests.
|
||
|
+cp "$CREDSDIR/127.0.0.1" "$CREDSDIR/localhost"
|
||
|
+
|
||
|
+begin_test "credentials rejected with line feed"
|
||
|
+(
|
||
|
+ set -e
|
||
|
+
|
||
|
+ reponame="protect-linefeed"
|
||
|
+ setup_remote_repo "$reponame"
|
||
|
+ clone_repo "$reponame" "$reponame"
|
||
|
+
|
||
|
+ contents="a"
|
||
|
+ contents_oid=$(calc_oid "$contents")
|
||
|
+
|
||
|
+ git lfs track "*.dat"
|
||
|
+ printf "%s" "$contents" >a.dat
|
||
|
+ git add .gitattributes a.dat
|
||
|
+ git commit -m "add a.dat"
|
||
|
+
|
||
|
+ # Using localhost instead of 127.0.0.1 in the LFS API URL ensures this URL
|
||
|
+ # is used when filling credentials rather than the Git remote URL, which
|
||
|
+ # would otherwise be used since it would have the same scheme and hostname.
|
||
|
+ gitserver="$(echo "$GITSERVER" | sed 's/127\.0\.0\.1/localhost/')"
|
||
|
+ testreponame="test%0a$reponame"
|
||
|
+ git config lfs.url "$gitserver/$testreponame.git/info/lfs"
|
||
|
+
|
||
|
+ GIT_TRACE=1 git lfs push origin main 2>&1 | tee push.log
|
||
|
+ if [ "0" -eq "${PIPESTATUS[0]}" ]; then
|
||
|
+ echo >&2 "fatal: expected 'git lfs push' to fail ..."
|
||
|
+ exit 1
|
||
|
+ fi
|
||
|
+ grep "batch response: Git credentials for $gitserver.* not found" push.log
|
||
|
+ grep "credential value for path contains newline" push.log
|
||
|
+ refute_server_object "$testreponame" "$contents_oid"
|
||
|
+
|
||
|
+ git config credential.protectProtocol false
|
||
|
+
|
||
|
+ GIT_TRACE=1 git lfs push origin main 2>&1 | tee push.log
|
||
|
+ if [ "0" -eq "${PIPESTATUS[0]}" ]; then
|
||
|
+ echo >&2 "fatal: expected 'git lfs push' to fail ..."
|
||
|
+ exit 1
|
||
|
+ fi
|
||
|
+ grep "batch response: Git credentials for $gitserver.* not found" push.log
|
||
|
+ grep "credential value for path contains newline" push.log
|
||
|
+ refute_server_object "$testreponame" "$contents_oid"
|
||
|
+)
|
||
|
+end_test
|
||
|
+
|
||
|
+begin_test "credentials rejected with carriage return"
|
||
|
+(
|
||
|
+ set -e
|
||
|
+
|
||
|
+ reponame="protect-return"
|
||
|
+ setup_remote_repo "$reponame"
|
||
|
+ clone_repo "$reponame" "$reponame"
|
||
|
+
|
||
|
+ contents="a"
|
||
|
+ contents_oid=$(calc_oid "$contents")
|
||
|
+
|
||
|
+ git lfs track "*.dat"
|
||
|
+ printf "%s" "$contents" >a.dat
|
||
|
+ git add .gitattributes a.dat
|
||
|
+ git commit -m "add a.dat"
|
||
|
+
|
||
|
+ # Using localhost instead of 127.0.0.1 in the LFS API URL ensures this URL
|
||
|
+ # is used when filling credentials rather than the Git remote URL, which
|
||
|
+ # would otherwise be used since it would have the same scheme and hostname.
|
||
|
+ gitserver="$(echo "$GITSERVER" | sed 's/127\.0\.0\.1/localhost/')"
|
||
|
+ testreponame="test%0d$reponame"
|
||
|
+ git config lfs.url "$gitserver/$testreponame.git/info/lfs"
|
||
|
+
|
||
|
+ GIT_TRACE=1 git lfs push origin main 2>&1 | tee push.log
|
||
|
+ if [ "0" -eq "${PIPESTATUS[0]}" ]; then
|
||
|
+ echo >&2 "fatal: expected 'git lfs push' to fail ..."
|
||
|
+ exit 1
|
||
|
+ fi
|
||
|
+ grep "batch response: Git credentials for $gitserver.* not found" push.log
|
||
|
+ grep "credential value for path contains carriage return" push.log
|
||
|
+ refute_server_object "$testreponame" "$contents_oid"
|
||
|
+
|
||
|
+ git config credential.protectProtocol false
|
||
|
+
|
||
|
+ git lfs push origin main 2>&1 | tee push.log
|
||
|
+ if [ "0" -ne "${PIPESTATUS[0]}" ]; then
|
||
|
+ echo >&2 "fatal: expected 'git lfs push' to succeed ..."
|
||
|
+ exit 1
|
||
|
+ fi
|
||
|
+ [ $(grep -c "Uploading LFS objects: 100% (1/1)" push.log) -eq 1 ]
|
||
|
+ assert_server_object "$testreponame" "$contents_oid"
|
||
|
+)
|
||
|
+end_test
|
||
|
+
|
||
|
+begin_test "credentials rejected with null byte"
|
||
|
+(
|
||
|
+ set -e
|
||
|
+
|
||
|
+ reponame="protect-null"
|
||
|
+ setup_remote_repo "$reponame"
|
||
|
+ clone_repo "$reponame" "$reponame"
|
||
|
+
|
||
|
+ contents="a"
|
||
|
+ contents_oid=$(calc_oid "$contents")
|
||
|
+
|
||
|
+ git lfs track "*.dat"
|
||
|
+ printf "%s" "$contents" >a.dat
|
||
|
+ git add .gitattributes a.dat
|
||
|
+ git commit -m "add a.dat"
|
||
|
+
|
||
|
+ # Using localhost instead of 127.0.0.1 in the LFS API URL ensures this URL
|
||
|
+ # is used when filling credentials rather than the Git remote URL, which
|
||
|
+ # would otherwise be used since it would have the same scheme and hostname.
|
||
|
+ gitserver="$(echo "$GITSERVER" | sed 's/127\.0\.0\.1/localhost/')"
|
||
|
+ testreponame="test%00$reponame"
|
||
|
+ git config lfs.url "$gitserver/$testreponame.git/info/lfs"
|
||
|
+
|
||
|
+ GIT_TRACE=1 git lfs push origin main 2>&1 | tee push.log
|
||
|
+ if [ "0" -eq "${PIPESTATUS[0]}" ]; then
|
||
|
+ echo >&2 "fatal: expected 'git lfs push' to fail ..."
|
||
|
+ exit 1
|
||
|
+ fi
|
||
|
+ grep "batch response: Git credentials for $gitserver.* not found" push.log
|
||
|
+ grep "credential value for path contains null byte" push.log
|
||
|
+ refute_server_object "$testreponame" "$contents_oid"
|
||
|
+
|
||
|
+ git config credential.protectProtocol false
|
||
|
+
|
||
|
+ GIT_TRACE=1 git lfs push origin main 2>&1 | tee push.log
|
||
|
+ if [ "0" -eq "${PIPESTATUS[0]}" ]; then
|
||
|
+ echo >&2 "fatal: expected 'git lfs push' to fail ..."
|
||
|
+ exit 1
|
||
|
+ fi
|
||
|
+ grep "batch response: Git credentials for $gitserver.* not found" push.log
|
||
|
+ grep "credential value for path contains null byte" push.log
|
||
|
+ refute_server_object "$testreponame" "$contents_oid"
|
||
|
+)
|
||
|
+end_test
|
||
|
diff -urN b/t/testhelpers.sh a/t/testhelpers.sh
|
||
|
--- b/t/testhelpers.sh 2023-12-13 19:56:25.000000000 +0100
|
||
|
+++ a/t/testhelpers.sh 2025-01-16 14:15:19.240279305 +0100
|
||
|
@@ -557,6 +557,14 @@
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
+
|
||
|
+setup_creds() {
|
||
|
+ mkdir -p "$CREDSDIR"
|
||
|
+ write_creds_file "user:pass" "$CREDSDIR/127.0.0.1"
|
||
|
+ write_creds_file ":pass" "$CREDSDIR/--$certpath"
|
||
|
+ write_creds_file ":pass" "$CREDSDIR/--$keypath"
|
||
|
+}
|
||
|
+
|
||
|
# setup initializes the clean, isolated environment for integration tests.
|
||
|
setup() {
|
||
|
cd "$ROOTDIR"
|
||
|
@@ -613,10 +621,7 @@
|
||
|
# setup the git credential password storage
|
||
|
local certpath="$(echo "$LFS_CLIENT_CERT_FILE" | tr / -)"
|
||
|
local keypath="$(echo "$LFS_CLIENT_KEY_FILE_ENCRYPTED" | tr / -)"
|
||
|
- mkdir -p "$CREDSDIR"
|
||
|
- write_creds_file "user:pass" "$CREDSDIR/127.0.0.1"
|
||
|
- write_creds_file ":pass" "$CREDSDIR/--$certpath"
|
||
|
- write_creds_file ":pass" "$CREDSDIR/--$keypath"
|
||
|
+ setup_creds
|
||
|
|
||
|
echo "#"
|
||
|
echo "# HOME: $HOME"
|