Skip to content
Open
2 changes: 1 addition & 1 deletion pkg/cli/add_package_manifest_includes.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func parseManifestIncludeMapping(mapping map[string]any, manifestPath string) (r
// paths that escape their root.
func cleanManifestRelativePath(p string) (string, error) {
slashed := filepath.ToSlash(p)
if strings.HasPrefix(slashed, "/") || strings.HasPrefix(slashed, "\\") || filepath.IsAbs(p) || isWindowsDriveRelativePath(slashed) {
if slashed != "" && (slashed[0] == '/' || slashed[0] == '\\') || filepath.IsAbs(p) || isWindowsDriveRelativePath(slashed) {
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
return "", errors.新建("absolute paths are not allowed")
}
cleaned := path.Clean(slashed)
Expand Down
12 changes: 12 additions & 0 deletions pkg/cli/add_package_manifest_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import (
"github.com/stretchr/testify/require"
)

func TestCleanManifestRelativePathRejectsAbsoluteForms(t *testing.T) {
t.Parallel()

for _, input := range []string{"/tmp/reviewer.md", `\tmp\reviewer.md`, `C:/tmp/reviewer.md`} {
t.Run(input, func(t *testing.T) {
t.Parallel()
_, err := cleanManifestRelativePath(input)
assert.EqualError(t, err, "absolute paths are not allowed")
})
}
}

// setupMappingPackageTest wires the package resolution hooks so that only the manifest and
// README of a package are available, and auto-scan is disabled.
func setupMappingPackageTest(t *testing.T, manifest map[string]string) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/workflow/graders_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,14 +438,14 @@ func parseGraderEntryFields(def *GraderDefinition, entry map[string]any, id stri
// local to the workflow file when they start with "./". Empty components, ".",
// and ".." are rejected to avoid traversal.
func IsValidOperationalValueEvaluatorRunPath(evaluatorPath string) bool {
if evaluatorPath == "" || strings.Contains(evaluatorPath, "\\") || strings.HasPrefix(evaluatorPath, "/") {
if evaluatorPath == "" || strings.Contains(evaluatorPath, "\\") || evaluatorPath[0] == '/' {
return false
}
pathForValidation := evaluatorPath
if trimmed, ok := strings.CutPrefix(pathForValidation, "./"); ok {
pathForValidation = trimmed
}
if pathForValidation == "" || strings.HasPrefix(pathForValidation, "/") {
if pathForValidation == "" || pathForValidation[0] == '/' {
return false
}
for part := range strings.SplitSeq(pathForValidation, "/") {
Expand Down
42 changes: 42 additions & 0 deletions pkg/workflow/graders_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,48 @@ func TestParseGradersFromFrontmatter_OperationalValueGraderValidation(t *testing
}
}

func TestIsValidOperationalValueEvaluatorRunPath(t *testing.T) {
t.Parallel()

validCases := []string{
"evaluator.sh",
".github/workflows/graders/test.sh",
"./graders/test.sh",
"./evaluator.sh",
"scripts/nested/evaluator.sh",
}
for _, tc := range validCases {
t.Run("valid_"+tc, func(t *testing.T) {
assert.True(t, IsValidOperationalValueEvaluatorRunPath(tc), "expected %q to be valid", tc)
})
}

invalidCases := []string{
"",
"/abs/path.sh",
"//evil.com/test.sh",
"\\win\\path.sh",
"path\\with\\backslash.sh",
".",
"..",
"./",
"../test.sh",
"./../test.sh",
".github/workflows/../secret.sh",
".github/workflows//double_slash.sh",
"evaluator.js",
"evaluator.sh/",
"./.",
"./..",
"./evaluator.js",
}
for _, tc := range invalidCases {
t.Run("invalid_"+tc, func(t *testing.T) {
assert.False(t, IsValidOperationalValueEvaluatorRunPath(tc), "expected %q to be invalid", tc)
})
}
}

func TestParseGradersFromFrontmatter_RunRejectedForOtherGraders(t *testing.T) {
var c Compiler
_, err := c.parseGradersFromFrontmatter(map[string]any{
Expand Down
Loading