Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix bug atmos vendor pull URI cannot contain path traversal sequences and git schema #899

Merged
merged 13 commits into from
Jan 22, 2025
40 changes: 40 additions & 0 deletions examples/tests/test-vendor/atmos.yaml
haitham911 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
base_path: "./"

components:
terraform:
base_path: "components/terraform"
apply_auto_approve: false
deploy_run_init: true
init_run_reconfigure: true
auto_generate_backend_file: false

stacks:
base_path: "stacks"
included_paths:
- "deploy/**/*"
excluded_paths:
- "**/_defaults.yaml"
name_pattern: "{stage}"

vendor:
# Single file
base_path: "./vendor.yaml"

# Directory with multiple files
#base_path: "./vendor"

# Absolute path
#base_path: "vendor.d/vendor1.yaml"

logs:
file: "/dev/stderr"
level: Info

# Custom CLI commands

# No arguments or flags are required
commands:
- name: "test"
description: "Run all tests"
steps:
- atmos vendor pull --everything
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Consider using explicit component selection instead of --everything flag

The --everything flag in the test command will pull all defined components:

  • github/stargazers (demo, github tags)
  • test-components (demo tag)
  • weather (demo tag)
  • my-vpc1 (no tags)

Instead of using --everything, consider one of these more explicit approaches:

  • Use --tags demo to pull all demo components
  • List specific components: atmos vendor pull -c github/stargazers -c weather
  • Create a separate test command for each component group you need to test

This provides better control over which components are pulled during testing and makes the dependencies more explicit.

🔗 Analysis chain

Review security implications of the --everything flag.

The --everything flag in the test command could potentially pull all vendor components without discrimination. Consider being more explicit about which components to test.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check what components would be pulled with --everything flag
atmos vendor pull --dry-run --everything

Length of output: 138


Script:

#!/bin/bash
# Let's check the vendor configuration and related files
fd vendor -t f

# Check for any documentation or configuration related to vendor components
rg -i "vendor" -g "*.{md,yaml,yml}" -A 3 -B 3

Length of output: 34075


Script:

#!/bin/bash
# Let's check the vendor.yaml file in the test-vendor directory to understand the configuration
cat examples/tests/test-vendor/vendor.yaml

# Let's also check if there are any specific components or tags defined
rg "component:|tags:" examples/tests/test-vendor/vendor.yaml -A 2

Length of output: 2092

🧰 Tools
🪛 yamllint (1.35.1)

[warning] 37-37: wrong indentation: expected 2 but found 0

(indentation)


[warning] 40-40: wrong indentation: expected 4 but found 2

(indentation)

13 changes: 13 additions & 0 deletions examples/tests/test-vendor/test-components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Example Terraform IPinfo Component

This Terraform module retrieves data from the IPinfo API for a specified IP address. If no IP address is specified, it retrieves data for the requester's IP address.

## Usage

haitham911 marked this conversation as resolved.
Show resolved Hide resolved
### Inputs

- `ip_address` (optional): The IP address to retrieve information for. If not specified, the requester's IP address will be used. The default value is an empty string.

### Outputs

- `metadata`: The data retrieved from IPinfo for the specified IP address, in JSON format.
7 changes: 7 additions & 0 deletions examples/tests/test-vendor/test-components/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
data "http" "ipinfo" {
url = var.ip_address != "" ? "https://ipinfo.io/${var.ip_address}" : "https://ipinfo.io"

request_headers = {
Accept = "application/json"
}
}
osterman marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions examples/tests/test-vendor/test-components/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output "metadata" {
description = "The data retrieved from IPinfo for the specified IP address"
value = jsondecode(data.http.ipinfo.response_body)
}
1 change: 1 addition & 0 deletions examples/tests/test-vendor/test-components/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
provider "http" {}
5 changes: 5 additions & 0 deletions examples/tests/test-vendor/test-components/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
variable "ip_address" {
description = "The IP address to retrieve information for (optional)"
type = string
default = ""
}
5 changes: 5 additions & 0 deletions examples/tests/test-vendor/test-components/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
terraform {
required_version = ">= 1.0.0"

required_providers {}
}
43 changes: 43 additions & 0 deletions examples/tests/test-vendor/vendor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
apiVersion: atmos/v1
kind: AtmosVendorConfig
metadata:
name: demo-vendoring
description: Atmos vendoring manifest for Atmos demo component library
spec:
# Import other vendor manifests, if necessary
imports: []

sources:
- component: "github/stargazers"
source: "github.com/cloudposse/atmos.git//examples/demo-library/{{ .Component }}?ref={{.Version}}"
version: "main"
targets:
- "components/terraform/{{ .Component }}/{{.Version}}"
included_paths:
- "**/*.tf"
- "**/*.tfvars"
- "**/*.md"
tags:
- demo
- github

- component: "test-components"
source: "file:///./test-components"
version: "main"
targets:
- "components/terraform/{{ .Component }}/{{.Version}}"
tags:
- demo

- component: "weather"
source: "git::https://github.com/cloudposse/atmos.git//examples/demo-library/{{ .Component }}?ref={{.Version}}"
version: "main"
targets:
- "components/terraform/{{ .Component }}/{{.Version}}"
tags:
- demo
- component: "my-vpc1"
source: "oci://public.ecr.aws/cloudposse/components/terraform/stable/aws/vpc:{{.Version}}"
version: "latest"
targets:
- "components/terraform/infra/my-vpc1"
osterman marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions internal/exec/vendor_component_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -248,6 +249,14 @@ func ExecuteComponentVendorInternal(
sourceIsLocalFile = true
}
}
u, err := url.Parse(uri)
if err == nil && u.Scheme != "" {
if u.Scheme == "file" {
trimmedPath := strings.TrimPrefix(filepath.ToSlash(u.Path), "/")
uri = filepath.Clean(trimmedPath)
useLocalFileSystem = true
}
}
}

var pType pkgType
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/vendor_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func downloadAndInstall(p *pkgAtmosVendor, dryRun bool, atmosConfig schema.Atmos
}
}
// Create temp directory
tempDir, err := os.MkdirTemp("", fmt.Sprintf("atmos-vendor-%d-*", time.Now().Unix()))
tempDir, err := os.MkdirTemp("", "atmos-vendor")
if err != nil {
return installedPkgMsg{
err: fmt.Errorf("failed to create temp directory: %w", err),
Expand Down
127 changes: 125 additions & 2 deletions internal/exec/vendor_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import (
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
"strings"

"github.com/bmatcuk/doublestar/v4"
tea "github.com/charmbracelet/bubbletea"
"github.com/hashicorp/go-getter"
cp "github.com/otiai10/copy"
"github.com/samber/lo"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -384,7 +386,7 @@
if err != nil {
return err
}
targetPath := filepath.Join(vendorConfigFilePath, target)
targetPath := filepath.Join(filepath.ToSlash(vendorConfigFilePath), filepath.ToSlash(target))
pkgName := s.Component
if pkgName == "" {
pkgName = uri
Expand Down Expand Up @@ -514,12 +516,22 @@
useLocalFileSystem := false
sourceIsLocalFile := false
if !useOciScheme {
if absPath, err := u.JoinAbsolutePathWithPath(vendorConfigFilePath, *uri); err == nil {
if absPath, err := u.JoinAbsolutePathWithPath(filepath.ToSlash(vendorConfigFilePath), *uri); err == nil {
uri = &absPath
useLocalFileSystem = true
sourceIsLocalFile = u.FileExists(*uri)
}
u, err := url.Parse(*uri)
if err == nil && u.Scheme != "" {
if u.Scheme == "file" {
trimmedPath := strings.TrimPrefix(filepath.ToSlash(u.Path), "/")
*uri = filepath.Clean(trimmedPath)
useLocalFileSystem = true
}
}

haitham911 marked this conversation as resolved.
Show resolved Hide resolved
}

return useOciScheme, useLocalFileSystem, sourceIsLocalFile
}

Expand Down Expand Up @@ -556,6 +568,8 @@
if filepath.Base(src) == ".git" {
return true, nil
}
tempDir = filepath.ToSlash(tempDir)
src = filepath.ToSlash(src)

trimmedSrc := u.TrimBasePathFromPath(tempDir+"/", src)

Expand Down Expand Up @@ -608,3 +622,112 @@
return false, nil
}
}

func validateURI(uri string) error {
if uri == "" {
return fmt.Errorf("URI cannot be empty")
}
if strings.Contains(uri, " ") {
return fmt.Errorf("URI cannot contain spaces")
}
// Validate scheme-specific format
if strings.HasPrefix(uri, "oci://") {
if !strings.Contains(uri[6:], "/") {
return fmt.Errorf("invalid OCI URI format")
}
}
return nil
}
haitham911 marked this conversation as resolved.
Show resolved Hide resolved
func isValidScheme(scheme string) bool {
validSchemes := map[string]bool{
"http": true,
"https": true,
"git": true,
"ssh": true,
"git::https": true,
}
return validSchemes[scheme]
}

// CustomGitHubDetector intercepts GitHub URLs and transforms them
// into something like git::https://<token>@github.com/... so we can
// do a git-based clone with a token.
type CustomGitHubDetector struct {

Check failure on line 655 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, linux)

CustomGitHubDetector redeclared in this block

Check failure on line 655 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (windows-latest, windows)

CustomGitHubDetector redeclared in this block

Check failure on line 655 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (macos-latest, macos)

CustomGitHubDetector redeclared in this block
AtmosConfig schema.AtmosConfiguration
}

// Detect implements the getter.Detector interface for go-getter v1.
func (d *CustomGitHubDetector) Detect(src, _ string) (string, bool, error) {

Check failure on line 660 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, linux)

method CustomGitHubDetector.Detect already declared at internal/exec/go_getter_utils.go:76:32

Check failure on line 660 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (windows-latest, windows)

method CustomGitHubDetector.Detect already declared at internal\exec\go_getter_utils.go:76:32

Check failure on line 660 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (macos-latest, macos)

method CustomGitHubDetector.Detect already declared at internal/exec/go_getter_utils.go:76:32
if len(src) == 0 {
return "", false, nil
}

if !strings.Contains(src, "://") {
src = "https://" + src
}

parsedURL, err := url.Parse(src)
if err != nil {
u.LogDebug(d.AtmosConfig, fmt.Sprintf("Failed to parse URL %q: %v\n", src, err))
return "", false, fmt.Errorf("failed to parse URL %q: %w", src, err)
}

if strings.ToLower(parsedURL.Host) != "github.com" {
u.LogDebug(d.AtmosConfig, fmt.Sprintf("Host is %q, not 'github.com', skipping token injection\n", parsedURL.Host))
return "", false, nil
}

parts := strings.SplitN(parsedURL.Path, "/", 4)
if len(parts) < 3 {
u.LogDebug(d.AtmosConfig, fmt.Sprintf("URL path %q doesn't look like /owner/repo\n", parsedURL.Path))
return "", false, fmt.Errorf("invalid GitHub URL %q", parsedURL.Path)
}

atmosGitHubToken := os.Getenv("ATMOS_GITHUB_TOKEN")
gitHubToken := os.Getenv("GITHUB_TOKEN")

var usedToken string
var tokenSource string

// 1. If ATMOS_GITHUB_TOKEN is set, always use that
if atmosGitHubToken != "" {
usedToken = atmosGitHubToken
tokenSource = "ATMOS_GITHUB_TOKEN"
u.LogDebug(d.AtmosConfig, "ATMOS_GITHUB_TOKEN is set\n")
} else {
// 2. Otherwise, only inject GITHUB_TOKEN if cfg.Settings.InjectGithubToken == true
if d.AtmosConfig.Settings.InjectGithubToken && gitHubToken != "" {
usedToken = gitHubToken
tokenSource = "GITHUB_TOKEN"
u.LogTrace(d.AtmosConfig, "InjectGithubToken=true and GITHUB_TOKEN is set, using it\n")
} else {
u.LogTrace(d.AtmosConfig, "No ATMOS_GITHUB_TOKEN or GITHUB_TOKEN found\n")
}
}

if usedToken != "" {
user := parsedURL.User.Username()
pass, _ := parsedURL.User.Password()
if user == "" && pass == "" {
u.LogDebug(d.AtmosConfig, fmt.Sprintf("Injecting token from %s for %s\n", tokenSource, src))
parsedURL.User = url.UserPassword("x-access-token", usedToken)
} else {
u.LogDebug(d.AtmosConfig, "Credentials found, skipping token injection\n")
}
}

finalURL := "git::" + parsedURL.String()

return finalURL, true, nil
}

// RegisterCustomDetectors prepends the custom detector so it runs before
// the built-in ones. Any code that calls go-getter should invoke this.
func RegisterCustomDetectors(atmosConfig schema.AtmosConfiguration) {

Check failure on line 726 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, linux)

RegisterCustomDetectors redeclared in this block

Check failure on line 726 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (windows-latest, windows)

RegisterCustomDetectors redeclared in this block

Check failure on line 726 in internal/exec/vendor_utils.go

View workflow job for this annotation

GitHub Actions / Build (macos-latest, macos)

RegisterCustomDetectors redeclared in this block
getter.Detectors = append(
[]getter.Detector{
&CustomGitHubDetector{AtmosConfig: atmosConfig},
},
getter.Detectors...,
)
}
haitham911 marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading