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
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
44 changes: 37 additions & 7 deletions internal/exec/vendor_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exec

import (
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -361,12 +362,20 @@ func ExecuteAtmosVendorInternal(
if err != nil {
return err
}
err = ValidateURI(uri)

useOciScheme, useLocalFileSystem, sourceIsLocalFile, err := determineSourceType(&uri, vendorConfigFilePath)
if err != nil {
return err
}

useOciScheme, useLocalFileSystem, sourceIsLocalFile := determineSourceType(&uri, vendorConfigFilePath)
if !useLocalFileSystem {
err = ValidateURI(uri)
if err != nil {
if strings.Contains(uri, "..") {
return fmt.Errorf("Invalid URI '%s': %w. Please ensure the source is a valid local path or a properly formatted URI.", uri, err)
}
return err
}
}

// Determine package type
var pType pkgType
Expand All @@ -384,7 +393,7 @@ func ExecuteAtmosVendorInternal(
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 @@ -504,7 +513,7 @@ func shouldSkipSource(s *schema.AtmosVendorSource, component string, tags []stri
return (component != "" && s.Component != component) || (len(tags) > 0 && len(lo.Intersect(tags, s.Tags)) == 0)
}

func determineSourceType(uri *string, vendorConfigFilePath string) (bool, bool, bool) {
func determineSourceType(uri *string, vendorConfigFilePath string) (bool, bool, bool, error) {
// Determine if the URI is an OCI scheme, a local file, or remote
useOciScheme := strings.HasPrefix(*uri, "oci://")
if useOciScheme {
Expand All @@ -514,13 +523,32 @@ func determineSourceType(uri *string, vendorConfigFilePath string) (bool, bool,
useLocalFileSystem := false
sourceIsLocalFile := false
if !useOciScheme {
if absPath, err := u.JoinAbsolutePathWithPath(vendorConfigFilePath, *uri); err == nil {
absPath, err := u.JoinAbsolutePathWithPath(filepath.ToSlash(vendorConfigFilePath), *uri)
// if URI contain path traversal is path should be resolved
if err != nil && strings.Contains(*uri, "..") {
return useOciScheme, useLocalFileSystem, sourceIsLocalFile, fmt.Errorf("invalid source path '%s': %w", *uri, err)
}
if err == nil {
uri = &absPath
useLocalFileSystem = true
sourceIsLocalFile = u.FileExists(*uri)
}

parsedURL, err := url.Parse(*uri)
if err != nil {
return useOciScheme, useLocalFileSystem, sourceIsLocalFile, err
}
if err == nil && parsedURL.Scheme != "" {
if parsedURL.Scheme == "file" {
trimmedPath := strings.TrimPrefix(filepath.ToSlash(parsedURL.Path), "/")
*uri = filepath.Clean(trimmedPath)
useLocalFileSystem = true
}
}

}
return useOciScheme, useLocalFileSystem, sourceIsLocalFile

return useOciScheme, useLocalFileSystem, sourceIsLocalFile, nil
}

func copyToTarget(atmosConfig schema.AtmosConfiguration, tempDir, targetPath string, s *schema.AtmosVendorSource, sourceIsLocalFile bool, uri string) error {
Expand Down Expand Up @@ -556,6 +584,8 @@ func generateSkipFunction(atmosConfig schema.AtmosConfiguration, tempDir string,
if filepath.Base(src) == ".git" {
return true, nil
}
tempDir = filepath.ToSlash(tempDir)
src = filepath.ToSlash(src)

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

Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/scenarios/complete/vendor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ spec:
- test
- networking
- component: "vpc-flow-logs-bucket"
source: "github.com/cloudposse/terraform-aws-components.git//modules/vpc-flow-logs-bucket?ref={{.Version}}"
source: "git::https://github.com/cloudposse/terraform-aws-components.git//modules/vpc-flow-logs-bucket?ref={{.Version}}"
version: "1.323.0"
targets:
- "components/terraform/infra/vpc-flow-logs-bucket/{{.Version}}"
Expand Down
40 changes: 40 additions & 0 deletions tests/fixtures/scenarios/vendor/atmos.yaml
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
13 changes: 13 additions & 0 deletions tests/fixtures/scenarios/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

### 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 tests/fixtures/scenarios/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"
}
}
4 changes: 4 additions & 0 deletions tests/fixtures/scenarios/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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
provider "http" {}
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 tests/fixtures/scenarios/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 {}
}
53 changes: 53 additions & 0 deletions tests/fixtures/scenarios/vendor/vendor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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"
- component: "vpc-src"
source: "../../../fixtures" # required for test URI contain path traversal
targets:
- "components/terraform/myapp"
included_paths:
- "**/*.tf"
- "**/*.md"
- "**/*.tftmpl"
- "**/modules/**"
excluded_paths: []
64 changes: 64 additions & 0 deletions tests/test-cases/demo-stacks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,67 @@ tests:
stderr:
- "^$"
exit_code: 0

- name: atmos greet with args
enabled: true
description: "Validate atmos custom command greet runs with argument provided."
workdir: "../examples/demo-custom-command/"
command: "atmos"
args:
- "greet"
- "Andrey"
expect:
stdout:
- "Hello, Andrey\n"
stderr:
- "^$"
exit_code: 0

- name: atmos greet without args
enabled: true
description: "Validate atmos custom command greet runs without argument provided."
workdir: "../examples/demo-custom-command/"
command: "atmos"
args:
- "greet"
expect:
stdout:
- "Hello, John Doe\n"
stderr:
- "^$"
exit_code: 0
- name: atmos_vendor_pull
enabled: true
description: "Ensure atmos vendor pull command executes without errors and files are present."
workdir: "fixtures/scenarios/vendor"
command: "atmos"
args:
- "vendor"
- "pull"
expect:
file_exists:
- "./components/terraform/github/stargazers/main/main.tf"
- "./components/terraform/github/stargazers/main/outputs.tf"
- "./components/terraform/github/stargazers/main/providers.tf"
- "./components/terraform/github/stargazers/main/variables.tf"
- "./components/terraform/github/stargazers/main/versions.tf"
- "./components/terraform/infra/my-vpc1/main.tf"
- "./components/terraform/infra/my-vpc1/outputs.tf"
- "./components/terraform/infra/my-vpc1/providers.tf"
- "./components/terraform/infra/my-vpc1/variables.tf"
- "./components/terraform/infra/my-vpc1/versions.tf"
- "./components/terraform/test-components/main/main.tf"
- "./components/terraform/test-components/main/outputs.tf"
- "./components/terraform/test-components/main/providers.tf"
- "./components/terraform/test-components/main/variables.tf"
- "./components/terraform/test-components/main/versions.tf"
- "./components/terraform/weather/main/main.tf"
- "./components/terraform/weather/main/outputs.tf"
- "./components/terraform/weather/main/providers.tf"
- "./components/terraform/weather/main/variables.tf"
- "./components/terraform/weather/main/versions.tf"
- "./components/terraform/vpc-src/main.tf"
- "./components/terraform/vpc-src/outputs.tf"
- "./components/terraform/vpc-src/variables.tf"
- "./components/terraform/vpc-src/versions.tf"
exit_code: 0
Loading