safesonnet

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 19, 2025 License: MIT Imports: 9 Imported by: 0

README

SafeSonnet

Go Reference test Go Report Card

SafeSonnet is a secure file importer for google/go-jsonnet that restricts file imports to a specific directory using os.Root functionality introduced in Go 1.24. This helps prevent path traversal attacks and ensures that Jsonnet imports can only access files within a designated directory.

See docs/spec.md for the full specification, with differences to the built-in go-jsonnet file importer.

Installation

go get github.com/thevilledev/safesonnet

Requires Go 1.24.

Usage

See example directory for a complete working example.

Basic usage:

importer, err := safesonnet.NewSafeImporter("jsonnet", []string{
    filepath.Join("jsonnet", "lib"), // Library path relative to workspace
})
if err != nil {
    log.Fatal(err)
}
// Close is required to release the os.Root file descriptor
defer importer.Close()

vm := jsonnet.MakeVM()
vm.Importer(importer)

Note: Unlike jsonnet.FileImporter, SafeImporter requires calling Close() to release the underlying os.Root file descriptor. Always use defer importer.Close() after creating the importer.

Security

SafeSonnet uses Go 1.24's os.Root functionality to ensure that file access is restricted to the specified directory tree. This means:

  • No access to files outside the specified root directory.
  • No following of symbolic links that point outside the root.
  • No absolute path traversal.
  • No relative path traversal (e.g., using ../).
  • Library paths (JPaths) must be within the root directory.

License

MIT License - see LICENSE file for full details.

Documentation

Overview

Example
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/google/go-jsonnet"
	"github.com/thevilledev/safesonnet"
)

func main() {
	// Create a temporary directory for our examples
	rootDir, err := os.MkdirTemp("", "safesonnet-example")
	if err != nil {
		fmt.Printf("Failed to create temp dir: %v\n", err)

		return
	}
	defer os.RemoveAll(rootDir)

	// Create some jsonnet files
	if err := os.MkdirAll(filepath.Join(rootDir, "lib"), 0755); err != nil {
		fmt.Printf("Failed to create lib dir: %v\n", err)

		return
	}

	mainJsonnet := `local utils = import 'utils.jsonnet';
{
  result: utils.add(40, 2)
}`

	utilsJsonnet := `{
  add(a, b): a + b,
}`

	if err := os.WriteFile(filepath.Join(rootDir, "main.jsonnet"), []byte(mainJsonnet), 0o600); err != nil {
		fmt.Printf("Failed to write main.jsonnet: %v\n", err)

		return
	}

	if err := os.WriteFile(filepath.Join(rootDir, "lib", "utils.jsonnet"), []byte(utilsJsonnet), 0o600); err != nil {
		fmt.Printf("Failed to write utils.jsonnet: %v\n", err)

		return
	}

	// Create a SafeImporter with the root directory and a library path
	importer, err := safesonnet.NewSafeImporter(rootDir, []string{filepath.Join(rootDir, "lib")})
	if err != nil {
		fmt.Printf("Failed to create importer: %v\n", err)

		return
	}
	defer importer.Close()

	// First, show how to import a file
	contents, foundAt, err := importer.Import("", "main.jsonnet")
	if err != nil {
		fmt.Printf("Failed to import: %v\n", err)

		return
	}

	fmt.Printf("Found file at: %s\n", filepath.Base(foundAt))
	fmt.Printf("Contents: %s\n", contents.String())

	// Now, evaluate the jsonnet code
	vm := jsonnet.MakeVM()
	vm.Importer(importer)

	// Evaluate the jsonnet code directly
	result, err := vm.EvaluateAnonymousSnippet("example.jsonnet", mainJsonnet)
	if err != nil {
		fmt.Printf("Failed to evaluate: %v\n", err)

		return
	}

	fmt.Printf("Evaluated result: %s\n", result)

}
Output:
Found file at: main.jsonnet
Contents: local utils = import 'utils.jsonnet';
{
  result: utils.add(40, 2)
}
Evaluated result: {
   "result": 42
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrJPathOutsideRoot is returned when a JPath is outside the root directory.
	ErrJPathOutsideRoot = errors.New("jpath is outside root directory")
	// ErrEmptyRootDir is returned when the root directory is empty.
	ErrEmptyRootDir = errors.New("root directory must not be empty")
	// ErrOpenRootDir is returned when the root directory cannot be opened.
	ErrOpenRootDir = errors.New("failed to open root directory")
	// ErrAbsPath is returned when the absolute path cannot be obtained.
	ErrAbsPath = errors.New("failed to get absolute path of root")
	// ErrAbsPathJPath is returned when the absolute path of a JPath cannot be obtained.
	ErrAbsPathJPath = errors.New("failed to get absolute path of jpath")
	// ErrRelPath is returned when the relative path cannot be obtained.
	ErrRelPath = errors.New("failed to get relative path of jpath")
	// ErrReadFile is returned when a file cannot be read.
	ErrReadFile = errors.New("failed to read file")
	// ErrOpenFile is returned when a file cannot be opened.
	ErrOpenFile = errors.New("failed to open file")
	// ErrRootAbsPath is returned when the root absolute path cannot be obtained.
	ErrRootAbsPath = errors.New("failed to get root absolute path")
	// ErrRelPathConversion is returned when a relative path cannot be obtained.
	ErrRelPathConversion = errors.New("failed to convert path to be relative to root")
	// ErrFileNotFound is returned when a file is not found in any library path.
	ErrFileNotFound = errors.New("file not found in any library path")
	// ErrCloseRootDir is returned when the root directory cannot be closed.
	ErrCloseRootDir = errors.New("failed to close root directory")
	// ErrCacheInternalType is returned when the cache contains an unexpected value type.
	ErrCacheInternalType = errors.New("internal cache error: unexpected type")
	// ErrForbiddenAbsolutePath is returned when an import path is absolute and outside the
	// root directory.
	ErrForbiddenAbsolutePath = errors.New("forbidden absolute import path")
	// ErrForbiddenRelativePathTraversal is returned when a relative import path attempts to
	// traverse outside the root directory.
	ErrForbiddenRelativePathTraversal = errors.New("forbidden relative import path traversal")
	// ErrInvalidNullByte is returned when a path contains a null byte, which is invalid.
	ErrInvalidNullByte = errors.New("path contains an invalid null byte")
)

Functions

This section is empty.

Types

type Option added in v1.0.0

type Option func(*SafeImporter)

Option is a functional option for configuring SafeImporter.

func WithLogger added in v1.0.0

func WithLogger(logger *log.Logger) Option

WithLogger allows providing a custom logger to SafeImporter. If nil is provided, the default discarding logger will be used.

type SafeImporter

type SafeImporter struct {
	// JPaths is a list of library search paths within the root directory.
	JPaths []string
	// contains filtered or unexported fields
}

SafeImporter implements jsonnet.Importer interface that restricts imports to a specific directory. It prevents path traversal attacks by ensuring all imports are within the specified root directory. The importer supports a list of library paths (JPaths) within the root directory, similar to the standard jsonnet importer. Caches file reads.

func NewSafeImporter

func NewSafeImporter(rootDir string, jpaths []string, opts ...Option) (*SafeImporter, error)

NewSafeImporter creates a new SafeImporter that restricts imports to the given directory. It validates that all provided JPaths are within the root directory to maintain the security boundary. If no JPaths are provided, the root directory is used as the only search path.

rootDir must be a valid directory path or an error will be returned. jpaths should be a list of directories inside rootDir to search for imports. opts can be used to configure the SafeImporter, e.g., by providing a logger.

func (*SafeImporter) Close

func (i *SafeImporter) Close() error

Close releases resources associated with the importer. This should be called when the importer is no longer needed to prevent resource leaks.

func (*SafeImporter) Import

func (i *SafeImporter) Import(importedFrom, importedPath string) (jsonnet.Contents, string, error)

Import implements jsonnet.Importer interface. It searches for the importedPath in several locations in order:

  1. Relative to the importing file (if importedFrom is provided)
  2. In the root directory (if importedPath is not absolute)
  3. In each of the JPaths

The method respects the security boundary and will not allow imports from outside the root directory.

Directories

Path Synopsis
Package main demonstrates the usage of safesonnet importer
Package main demonstrates the usage of safesonnet importer

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL