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

feat: disable file:// urls when hardening enabled #24858

Open
wants to merge 17 commits into
base: main-2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions cmd/influxd/launcher/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,9 +647,10 @@ func (o *InfluxdOpts) BindCliOpts() []cli.Opt {
},

// hardening options
// --hardening-enabled is meant to enable all hardending
// --hardening-enabled is meant to enable all hardening
// options in one go. Today it enables the IP validator for
// flux and pkger templates HTTP requests. In the future,
// flux and pkger templates HTTP requests, and disables file://
// protocol for pkger templates. In the future,
// --hardening-enabled might be used to enable other security
// features, at which point we can add per-feature flags so
// that users can either opt into all features
Expand All @@ -661,7 +662,7 @@ func (o *InfluxdOpts) BindCliOpts() []cli.Opt {
DestP: &o.HardeningEnabled,
Flag: "hardening-enabled",
Default: o.HardeningEnabled,
Desc: "enable hardening options (disallow private IPs within flux and templates HTTP requests, template file URLs)",
Desc: "enable hardening options (disallow private IPs within flux and templates HTTP requests; disable file URLs in templates)",
},

// --template-file-urls-disabled prevents file protocol URIs
Expand Down
57 changes: 40 additions & 17 deletions pkg/fs/special_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fs
import (
"errors"
"io/fs"
"math"
"os"
"syscall"

Expand All @@ -16,7 +17,7 @@ func IsSpecialFSFromFileInfo(st fs.FileInfo) (bool, error) {
// On Linux, special file systems like /proc, /dev/, and /sys are
// considered unnamed devices (non-device mounts). These devices
// will always have a major device number of 0 per the kernels
// Documentation/devices.txt file.
// Documentation/admin-guide/devices.txt file.

getDevId := func(st fs.FileInfo) (uint64, error) {
st_sys_any := st.Sys()
Expand All @@ -41,23 +42,45 @@ func IsSpecialFSFromFileInfo(st fs.FileInfo) (bool, error) {
}

// We know the file is in a special file system, but we'll make an
// exception for tmpfs, which some distros use for /tmp.
// exception for tmpfs, which might be used at a variety of mount points.
// Since the minor IDs are assigned dynamically, we'll find the device ID
// for /tmp. If /tmp's device ID matches this st's, then it is safe to assume
// the file is in tmpfs. Otherwise, it is a special device that is not tmpfs.
// Note that if /tmp is not tmpfs, then the device IDs won't match since
// tmp would be a standard file system.
tmpSt, err := os.Stat(os.TempDir())
if err != nil {
// We got an error getting stats on /tmp, but that just means we can't
// say the file is in tmpfs. We'll still go ahead and call it a special file.
return true, nil
// for each common tmpfs mount point, If the mount point's device ID matches this st's,
gwossum marked this conversation as resolved.
Show resolved Hide resolved
// then it is reasonable to assume the file is in tmpfs. If the device ID
// does not match, then st is not located in that special file system so we
// can't give an exception based on that file system root. This check is still
// valid even if the directory we check against isn't mounted as tmpfs, because
// the device ID won't match so we won't grant a tmpfs exception based on it.
// On Linux, every tmpfs mount has a different device ID, so we need to check
// against all common ones that might be in use.
tmpfsMounts := []string{"/tmp", "/run", "/dev/shm"}
gwossum marked this conversation as resolved.
Show resolved Hide resolved
if tmpdir := os.TempDir(); tmpdir != "/tmp" {
tmpfsMounts = append(tmpfsMounts, tmpdir)
}
tmpDevId, err := getDevId(tmpSt)
if err != nil {
// See above for why we're returning an error here.
return true, nil
if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
tmpfsMounts = append(tmpfsMounts, xdgRuntimeDir)
}
getFileDevId := func(n string) (uint64, error) {
fSt, err := os.Stat(n)
if err != nil {
return math.MaxUint64, err
}
fDevId, err := getDevId(fSt)
if err != nil {
// See above for why we're returning an error here.
return math.MaxUint64, nil
gwossum marked this conversation as resolved.
Show resolved Hide resolved
}
return fDevId, nil
}
for _, fn := range tmpfsMounts {
// We ignore errors if getFileDevId fails, which could
// mean that the file (e.g. /run) doesn't exist. The error
gwossum marked this conversation as resolved.
Show resolved Hide resolved
if fnDevId, err := getFileDevId(fn); err == nil {
if fnDevId == devId {
return false, nil
}
}
}
// It's a special file unless the device ID matches /tmp's ID.
return devId != tmpDevId, nil

// We didn't find any a reason to give st a special file system exception.
return true, nil
}
6 changes: 0 additions & 6 deletions pkger/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,6 @@ const limitReadFileMaxSize int64 = 2 * 1024 * 1024
// contents. A successful call returns err == nil, not err == EOF. Because
// limitReadFile reads the whole file, it does not treat an EOF from Read as an
// error to be reported.
//
// Ultimately, only remocal will have file:// URLs enabled for its 'data
// catalogs' bootstrap functionality. At present, the largest catalog is 40k,
// so set a small max here with a bit of headroom.
func limitReadFile(name string) (buf []byte, rErr error) {
// use os.Open() to avoid TOCTOU
f, err := os.Open(name)
Expand Down Expand Up @@ -149,8 +145,6 @@ func limitReadFile(name string) (buf []byte, rErr error) {
if size64 > limitReadFileMaxSize {
gwossum marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("file too big: %q", name)
} else if size64 == 0 {
// A lot of /proc files report their length as 0, so this will also
// catch a large proportion of /proc files.
return nil, fmt.Errorf("file empty: %q", name)
}
size = int(size64)
Expand Down
98 changes: 96 additions & 2 deletions pkger/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4829,6 +4829,7 @@ func Test_validGeometry(t *testing.T) {

func Test_FromFile(t *testing.T) {
dir := t.TempDir()

// create empty test file
emptyFn := filepath.Join(dir, "empty")
fe, err := os.Create(emptyFn)
Expand All @@ -4843,12 +4844,15 @@ func Test_FromFile(t *testing.T) {
err = os.Truncate(bigFn, limitReadFileMaxSize+1)
assert.Nil(t, err)

tests := []struct {
type testCase struct {
path string
extra bool
expErr string
oses []string // list of OSes to run test on, empty means all.
}{
}

// Static test suites
tests := []testCase{
// valid
{
path: "testdata/bucket_schema.yml",
Expand Down Expand Up @@ -4921,6 +4925,96 @@ func Test_FromFile(t *testing.T) {
},
}

// Add tmpfs special file system exception tests for Linux.
// We don't consider errors creating the tests cases (e.g. no tmpfs mounts, no write
// permissions to any tmpfs mount) errors for the test itself. It's possible we
// can't run these tests in some locked down environments, but we will warn the
// use we couldn't run the tests.
if runtime.GOOS == "linux" {
tmpfsDirs, err := func() ([]string, error) {
t.Helper()
// Quick and dirty parse of /proc/mounts to find tmpfs mount points on system.
mounts, err := os.ReadFile("/proc/mounts")
if err != nil {
return nil, fmt.Errorf("error reading /proc/mounts: %w", err)
}
mountsLines := strings.Split(string(mounts), "\n")
var tmpfsMounts []string
for _, line := range mountsLines {
if line == "" {
continue
}
cols := strings.Split(line, " ")
if len(cols) != 6 {
return nil, fmt.Errorf("unexpected /proc/mounts line format (%d columns): %q", len(cols), line)
}
if cols[0] == "tmpfs" {
tmpfsMounts = append(tmpfsMounts, cols[1])
}
}
if len(tmpfsMounts) == 0 {
return nil, errors.New("no tmpfs mount points found")
}

// Find which common tmpfs directories are actually mounted as tmpfs.
candidateDirs := []string{
"/dev/shm",
"/run",
"/tmp",
os.Getenv("XDG_RUNTIME_DIR"),
os.TempDir(),
}
var actualDirs []string
for _, dir := range candidateDirs {
if dir == "" {
continue
}
for _, mount := range tmpfsMounts {
if strings.HasPrefix(dir, mount) {
actualDirs = append(actualDirs, dir)
}
}
}
if len(actualDirs) == 0 {
return nil, errors.New("no common tmpfs directories on tmpfs mount points")
}
return actualDirs, nil
}()
if err == nil {
// Create test files in the tmpfs directories and create test cases
var tmpfsTests []testCase
var tmpfsErrs []error
contents, err := os.ReadFile("testdata/bucket_schema.yml")
require.NoError(t, err)
require.NotEmpty(t, contents)
for _, dir := range tmpfsDirs {
testFile, err := os.CreateTemp(dir, "fromfile_*")
if err == nil {
testPath := testFile.Name()
defer os.Remove(testPath)
_, err = testFile.Write(contents)
require.NoError(t, err)
require.NoError(t, testFile.Close())
tmpfsTests = append(tmpfsTests, testCase{
path: testPath,
extra: true,
expErr: "",
})
} else {
tmpfsErrs = append(tmpfsErrs, fmt.Errorf("error tmpfs test file in %q: %w", dir, err))
}
}
// Ignore errors creating tmpfs files if we got at least one test case from the bunch
if len(tmpfsTests) > 0 {
tests = append(tests, tmpfsTests...)
} else {
t.Log(fmt.Sprintf("WARNING: could not create files for tmpfs special file system tests: %s", errors.Join(tmpfsErrs...)))
}
} else {
t.Log(fmt.Sprintf("WARNING: unable to run tmpfs special file system tests: %s", err))
}
}

for _, tt := range tests {
fn := func(t *testing.T) {
if len(tt.oses) > 0 {
Expand Down