Skip to content

Conversation

@thc1006
Copy link

@thc1006 thc1006 commented Oct 8, 2025

Implements #107: Add possibility to untar policy content to disk when pulling from OCI registry.

This change simplifies the workflow from 3 steps to 1 step:

  • Before: policy pull + policy save + tar extraction
  • After: policy pull --untar --untardir

Changes:

  • Add --untar flag to enable automatic extraction after pull
  • Add --untardir flag to specify extraction directory (default: ".")
  • Implement ExtractPolicyBundle() with security features:
    • Path traversal attack prevention
    • Safe symlink handling
    • Directory creation on demand
  • Add ErrExtractFailed error type
  • Update Pull() method signature to support new parameters
  • Maintain backward compatibility (existing code unchanged)

Security:

  • Validates all file paths to prevent writing outside target directory
  • Safely handles symlinks by checking resolved targets
  • Skips unknown file types with warnings

Inspired by Helm's helm pull --untar --untardir functionality.

Testing:

  • Backward compatibility verified (existing tests pass)
  • Code compiles successfully
  • Help text displays new flags correctly

Implements opcr-io#107: Add possibility to untar policy content to disk when
pulling from OCI registry.

This change simplifies the workflow from 3 steps to 1 step:
- Before: policy pull + policy save + tar extraction
- After: policy pull --untar --untardir <dir>

Changes:
- Add --untar flag to enable automatic extraction after pull
- Add --untardir flag to specify extraction directory (default: ".")
- Implement ExtractPolicyBundle() with security features:
  * Path traversal attack prevention
  * Safe symlink handling
  * Directory creation on demand
- Add ErrExtractFailed error type
- Update Pull() method signature to support new parameters
- Maintain backward compatibility (existing code unchanged)

Security:
- Validates all file paths to prevent writing outside target directory
- Safely handles symlinks by checking resolved targets
- Skips unknown file types with warnings

Inspired by Helm's `helm pull --untar --untardir` functionality.

Testing:
- Backward compatibility verified (existing tests pass)
- Code compiles successfully
- Help text displays new flags correctly

Signed-off-by: thc1006 <[email protected]>

type PullCmd struct {
Policies []string `arg:"" name:"policy" help:"Policies to pull from the remote registry."`
Untar bool `name:"untar" help:"Untar the policy bundle after pulling."`
Copy link
Contributor

@gertd gertd Oct 8, 2025

Choose a reason for hiding this comment

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

Why have two flags? It feels redundant, as you always have to validate UntarDir, so you might as well use the fact that when UntarDir is set instead of --untar

I do not think there exists a real use case where --untardir is set and --untar == false

type PullCmd struct {
Policies []string `arg:"" name:"policy" help:"Policies to pull from the remote registry."`
Untar bool `name:"untar" help:"Untar the policy bundle after pulling."`
UntarDir string `name:"untardir" help:"Directory to extract the policy bundle." default:"." type:"path"`
Copy link
Contributor

Choose a reason for hiding this comment

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

From a security POV, I would require that --untardir must point to an existing directory.

To enforce this, please use type:"existingdir" instead of type:"path" to leverage the existence check present in kong package.

}()

// Create destination directory if it doesn't exist
if err := os.MkdirAll(destDir, 0755); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

The os.MkdirAll call is not needed when we enforce existance of --untardir

If we still need this, please use the existing filemode constant x.OwnerReadWriteExecute

}

// Create and write file
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be an os.Create() call?

// Security check: prevent path traversal attacks
targetPath := filepath.Join(absDestDir, header.Name)
if !isPathSafe(targetPath, absDestDir) {
c.UI.Problem().Msgf("Skipping unsafe path: %s", header.Name)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would abort in this case

Based on review feedback from @gertd:

1. Remove redundant --untar flag
   - Only keep --untardir flag
   - Bundle extraction is automatic when --untardir is set
   - Simplifies the interface and removes redundancy

2. Enhance security and validation
   - Change --untardir type from "path" to "existingdir" for built-in validation
   - Require destination directory to exist before extraction
   - Abort on unsafe paths instead of skipping with warnings
   - Validate absolute path existence immediately

3. Improve code quality
   - Use os.Create() instead of os.OpenFile() for file creation
   - Use x.OwnerReadWriteExecute constant for directory permissions
   - Remove unnecessary os.MkdirAll for validated existing directory

These changes maintain backward compatibility while improving security
and simplifying the user interface.

Signed-off-by: thc1006 <[email protected]>
@thc1006
Copy link
Author

thc1006 commented Oct 9, 2025

Hi @gertd,

Thank you for your thorough review and valuable feedback! I've implemented all your suggestions:

Changes Made:

1. Simplified Flag Interface

  • ✅ Removed the redundant --untar flag
  • ✅ Now only --untardir is needed - when set, extraction happens automatically
  • This makes the interface cleaner and more intuitive

2. Enhanced Security

  • ✅ Changed --untardir type from "path" to "existingdir" for built-in validation
  • ✅ Added immediate validation that the destination directory exists
  • ✅ Changed from warnings to hard failures when unsafe paths are detected
  • ✅ Both regular paths and symlinks now abort on security violations

3. Improved Code Quality

  • ✅ Replaced os.OpenFile() with os.Create() for file creation
  • ✅ Using x.OwnerReadWriteExecute constant for directory permissions
  • ✅ Removed unnecessary os.MkdirAll since we now require existing directories

Result:

The implementation still fully addresses issue #107's requirement to extract policy content in a single step, but with a simpler and more secure approach. Users can now simply run:

policy pull <ref> --untardir ./existing-dir

Instead of the original three-step process (pull → save → tar).

All changes have been tested and the code compiles successfully. The DCO sign-off has also been added to the commit.

Thank you again for your insightful review - these changes significantly improve both the usability and security of the feature!

@thc1006
Copy link
Author

thc1006 commented Oct 24, 2025

Dear @gertd is this apporach LGFU? Happy to adjust if you'd prefer a different structure!

}

// Security check: prevent path traversal attacks
targetPath := filepath.Join(absDestDir, header.Name)

Check failure

Code scanning / CodeQL

Arbitrary file write extracting an archive containing symbolic links High

Unresolved path from an archive header, which may point outside the archive root, is used in
symlink creation
.
Copy link
Contributor

Choose a reason for hiding this comment

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

@thc1006 can you please look into the linter issues


case tar.TypeSymlink:
// Handle symlinks carefully - ensure they don't point outside destDir
linkTarget := header.Linkname

Check failure

Code scanning / CodeQL

Arbitrary file write extracting an archive containing symbolic links High

Unresolved path from an archive header, which may point outside the archive root, is used in
symlink creation
.
gertd
gertd previously approved these changes Oct 30, 2025
@gertd gertd dismissed their stale review October 30, 2025 10:42

Please address linter and security scan issues

Security fixes:
- Add comprehensive path sanitization in ExtractPolicyBundle
- Validate and reject absolute paths in tar archives
- Validate and reject path traversal attempts (..)
- Sanitize symlink targets to prevent symlink-based attacks
- Implement isPathSafe helper for final safety checks

This addresses the CodeQL warnings about "Arbitrary file write
extracting an archive containing symbolic links" (CWE-22, CWE-59).

Linter fixes:
- Add nolint directives for legitimate security validation complexity
- Fix missing periods in comments (godot)
- Shorten help text to meet line length requirements (lll)
- Add proper whitespace between logical blocks (wsl)
- Run gofmt on all Go files for consistent formatting

Testing:
- All unit tests passing (pkg/app, oci, parser)
- go vet: clean
- go build: successful
- golangci-lint: clean (only pre-existing nolintlint in login.go)
- goreleaser build: successful

Fixes opcr-io#206

Signed-off-by: thc1006 <[email protected]>
@thc1006
Copy link
Author

thc1006 commented Oct 30, 2025

Dear @gertd All linter and security issues have been addressed in commit e183925:

Security fixes:

  • Added path sanitization for header.Name and header.Linkname
  • Reject absolute paths and path traversal attempts
  • Fixes CodeQL warnings (CWE-22, CWE-59)

Linter fixes:

  • Added nolint directives with justifications
  • Fixed comment formatting (godot)
  • Shortened help text (lll)
  • Added whitespace (wsl)
  • Ran gofmt on all files

@thc1006 thc1006 requested a review from gertd November 13, 2025 04:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants