Skip to content

Commit 852a03d

Browse files
authored
feat: disable file:// urls when hardening enabled (#25069)
Stacks and templates allow specifying file:// URLs. Add command line option `--template-file-urls-disabled` to disable their use for people who don't require them. Closes: #25068 (cherry picked from commit 9fd91a5)
1 parent c6c00b8 commit 852a03d

File tree

8 files changed

+876
-33
lines changed

8 files changed

+876
-33
lines changed

cmd/influxd/launcher/cmd.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,11 @@ type InfluxdOpts struct {
190190

191191
Viper *viper.Viper
192192

193+
// HardeningEnabled toggles multiple best-practice hardening options on.
193194
HardeningEnabled bool
194-
StrongPasswords bool
195+
// TemplateFileUrlsDisabled disables file protocol URIs in templates.
196+
TemplateFileUrlsDisabled bool
197+
StrongPasswords bool
195198
}
196199

197200
// NewOpts constructs options with default values.
@@ -243,8 +246,9 @@ func NewOpts(viper *viper.Viper) *InfluxdOpts {
243246
Testing: false,
244247
TestingAlwaysAllowSetup: false,
245248

246-
HardeningEnabled: false,
247-
StrongPasswords: false,
249+
HardeningEnabled: false,
250+
TemplateFileUrlsDisabled: false,
251+
StrongPasswords: false,
248252
}
249253
}
250254

@@ -643,9 +647,10 @@ func (o *InfluxdOpts) BindCliOpts() []cli.Opt {
643647
},
644648

645649
// hardening options
646-
// --hardening-enabled is meant to enable all hardending
650+
// --hardening-enabled is meant to enable all hardening
647651
// options in one go. Today it enables the IP validator for
648-
// flux and pkger templates HTTP requests. In the future,
652+
// flux and pkger templates HTTP requests, and disables file://
653+
// protocol for pkger templates. In the future,
649654
// --hardening-enabled might be used to enable other security
650655
// features, at which point we can add per-feature flags so
651656
// that users can either opt into all features
@@ -657,7 +662,16 @@ func (o *InfluxdOpts) BindCliOpts() []cli.Opt {
657662
DestP: &o.HardeningEnabled,
658663
Flag: "hardening-enabled",
659664
Default: o.HardeningEnabled,
660-
Desc: "enable hardening options (disallow private IPs within flux and templates HTTP requests)",
665+
Desc: "enable hardening options (disallow private IPs within flux and templates HTTP requests; disable file URLs in templates)",
666+
},
667+
668+
// --template-file-urls-disabled prevents file protocol URIs
669+
// from being used for templates.
670+
{
671+
DestP: &o.TemplateFileUrlsDisabled,
672+
Flag: "template-file-urls-disabled",
673+
Default: o.TemplateFileUrlsDisabled,
674+
Desc: "disable template file URLs",
661675
},
662676
{
663677
DestP: &o.StrongPasswords,

cmd/influxd/launcher/launcher.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,8 +752,10 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
752752
authedOrgSVC := authorizer.NewOrgService(b.OrganizationService)
753753
authedUrmSVC := authorizer.NewURMService(b.OrgLookupService, b.UserResourceMappingService)
754754
pkgerLogger := m.log.With(zap.String("service", "pkger"))
755+
disableFileUrls := opts.HardeningEnabled || opts.TemplateFileUrlsDisabled
755756
pkgSVC = pkger.NewService(
756757
pkger.WithHTTPClient(pkger.NewDefaultHTTPClient(urlValidator)),
758+
pkger.WithFileUrlsDisabled(disableFileUrls),
757759
pkger.WithLogger(pkgerLogger),
758760
pkger.WithStore(pkger.NewStoreKV(m.kvStore)),
759761
pkger.WithBucketSVC(authorizer.NewBucketService(b.BucketService)),

pkg/fs/special.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !linux
2+
// +build !linux
3+
4+
package fs
5+
6+
import "io/fs"
7+
8+
// IsSpecialFSFromFileInfo determines if a file resides on a special file
9+
// system (e.g. /proc, /dev/, /sys) based on its fs.FileInfo.
10+
// The bool return value should be ignored if err is not nil.
11+
func IsSpecialFSFromFileInfo(st fs.FileInfo) (bool, error) {
12+
return false, nil
13+
}

pkg/fs/special_linux.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package fs
2+
3+
import (
4+
"errors"
5+
"io/fs"
6+
"math"
7+
"os"
8+
"syscall"
9+
10+
"golang.org/x/sys/unix"
11+
)
12+
13+
// IsSpecialFSFromFileInfo determines if a file resides on a special file
14+
// system (e.g. /proc, /dev/, /sys) based on its fs.FileInfo.
15+
// The bool return value should be ignored if err is not nil.
16+
func IsSpecialFSFromFileInfo(st fs.FileInfo) (bool, error) {
17+
// On Linux, special file systems like /proc, /dev/, and /sys are
18+
// considered unnamed devices (non-device mounts). These devices
19+
// will always have a major device number of 0 per the kernels
20+
// Documentation/admin-guide/devices.txt file.
21+
22+
getDevId := func(st fs.FileInfo) (uint64, error) {
23+
st_sys_any := st.Sys()
24+
if st_sys_any == nil {
25+
return 0, errors.New("nil returned by fs.FileInfo.Sys")
26+
}
27+
28+
st_sys, ok := st_sys_any.(*syscall.Stat_t)
29+
if !ok {
30+
return 0, errors.New("could not convert st.sys() to a *syscall.Stat_t")
31+
}
32+
return st_sys.Dev, nil
33+
}
34+
35+
devId, err := getDevId(st)
36+
if err != nil {
37+
return false, err
38+
}
39+
if unix.Major(devId) != 0 {
40+
// This file is definitely not on a special file system.
41+
return false, nil
42+
}
43+
44+
// We know the file is in a special file system, but we'll make an
45+
// exception for tmpfs, which might be used at a variety of mount points.
46+
// Since the minor IDs are assigned dynamically, we'll find the device ID
47+
// for each common tmpfs mount point. If the mount point's device ID matches this st's,
48+
// then it is reasonable to assume the file is in tmpfs. If the device ID
49+
// does not match, then st is not located in that special file system so we
50+
// can't give an exception based on that file system root. This check is still
51+
// valid even if the directory we check against isn't mounted as tmpfs, because
52+
// the device ID won't match so we won't grant a tmpfs exception based on it.
53+
// On Linux, every tmpfs mount has a different device ID, so we need to check
54+
// against all common ones that might be in use.
55+
tmpfsMounts := []string{"/tmp", "/run", "/dev/shm"}
56+
if tmpdir := os.TempDir(); tmpdir != "/tmp" {
57+
tmpfsMounts = append(tmpfsMounts, tmpdir)
58+
}
59+
if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
60+
tmpfsMounts = append(tmpfsMounts, xdgRuntimeDir)
61+
}
62+
getFileDevId := func(n string) (uint64, error) {
63+
fSt, err := os.Stat(n)
64+
if err != nil {
65+
return math.MaxUint64, err
66+
}
67+
fDevId, err := getDevId(fSt)
68+
if err != nil {
69+
return math.MaxUint64, err
70+
}
71+
return fDevId, nil
72+
}
73+
var errs []error
74+
for _, fn := range tmpfsMounts {
75+
// Don't stop if getFileDevId returns an error. It could
76+
// be because the tmpfsMount we're checking doesn't exist,
77+
// which shouldn't prevent us from checking the other
78+
// potential mount points.
79+
if fnDevId, err := getFileDevId(fn); err == nil {
80+
if fnDevId == devId {
81+
return false, nil
82+
}
83+
} else if !errors.Is(err, os.ErrNotExist) {
84+
// Ignore errors for missing mount points.
85+
errs = append(errs, err)
86+
}
87+
}
88+
89+
// We didn't find any a reason to give st a special file system exception.
90+
return true, errors.Join(errs...)
91+
}

pkger/parser.go

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
fluxurl "github.com/influxdata/flux/dependencies/url"
2424
"github.com/influxdata/flux/parser"
2525
errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
26+
caperr "github.com/influxdata/influxdb/v2/pkg/errors"
27+
"github.com/influxdata/influxdb/v2/pkg/fs"
2628
"github.com/influxdata/influxdb/v2/pkg/jsonnet"
2729
"github.com/influxdata/influxdb/v2/task/options"
2830
"gopkg.in/yaml.v3"
@@ -102,8 +104,65 @@ func Parse(encoding Encoding, readerFn ReaderFn, opts ...ValidateOptFn) (*Templa
102104
return pkg, nil
103105
}
104106

105-
// FromFile reads a file from disk and provides a reader from it.
106-
func FromFile(filePath string) ReaderFn {
107+
// limitReadFileMaxSize is the maximum file size that limitReadFile will read.
108+
const limitReadFileMaxSize int64 = 2 * 1024 * 1024
109+
110+
// limitReadFile operates like ioutil.ReadFile() in that it reads the contents
111+
// of a file into RAM, but will only read regular files up to the specified
112+
// max. limitReadFile reads the file named by filename and returns the
113+
// contents. A successful call returns err == nil, not err == EOF. Because
114+
// limitReadFile reads the whole file, it does not treat an EOF from Read as an
115+
// error to be reported.
116+
func limitReadFile(name string) (buf []byte, rErr error) {
117+
// use os.Open() to avoid TOCTOU
118+
f, err := os.Open(name)
119+
if err != nil {
120+
return nil, err
121+
}
122+
defer caperr.Capture(&rErr, f.Close)()
123+
124+
// Check that properties of file are OK.
125+
st, err := f.Stat()
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
// Disallow reading from special file systems (e.g. /proc, /sys/, /dev).
131+
if special, err := fs.IsSpecialFSFromFileInfo(st); err != nil {
132+
return nil, fmt.Errorf("%w: %q", err, name)
133+
} else if special {
134+
return nil, fmt.Errorf("file in special file system: %q", name)
135+
}
136+
137+
// only support reading regular files
138+
if st.Mode()&os.ModeType != 0 {
139+
return nil, fmt.Errorf("not a regular file: %q", name)
140+
}
141+
142+
// limit how much we read into RAM
143+
var size int
144+
size64 := st.Size()
145+
if limitReadFileMaxSize > 0 && size64 > limitReadFileMaxSize {
146+
return nil, fmt.Errorf("file too big: %q", name)
147+
} else if size64 == 0 {
148+
return nil, fmt.Errorf("file empty: %q", name)
149+
}
150+
size = int(size64)
151+
152+
// Read file
153+
data := make([]byte, size)
154+
b, err := f.Read(data)
155+
if err != nil {
156+
return nil, err
157+
} else if b != size {
158+
return nil, fmt.Errorf("short read: %q", name)
159+
}
160+
161+
return data, nil
162+
}
163+
164+
// fromFile reads a file from disk and provides a reader from it.
165+
func FromFile(filePath string, extraFileChecks bool) ReaderFn {
107166
return func() (io.Reader, string, error) {
108167
u, err := url.Parse(filePath)
109168
if err != nil {
@@ -118,9 +177,15 @@ func FromFile(filePath string) ReaderFn {
118177
}
119178

120179
// not using os.Open to avoid having to deal with closing the file in here
121-
b, err := os.ReadFile(u.Path)
122-
if err != nil {
123-
return nil, filePath, err
180+
var b []byte
181+
var rerr error
182+
if extraFileChecks {
183+
b, rerr = limitReadFile(u.Path)
184+
} else {
185+
b, rerr = os.ReadFile(u.Path)
186+
}
187+
if rerr != nil {
188+
return nil, filePath, rerr
124189
}
125190

126191
return bytes.NewBuffer(b), u.String(), nil
@@ -260,7 +325,7 @@ func parseSource(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
260325
b = bb
261326
}
262327

263-
contentType := http.DetectContentType(b[:512])
328+
contentType := http.DetectContentType(b[:min(len(b), 512)])
264329
switch {
265330
case strings.Contains(contentType, "jsonnet"):
266331
// highly unlikely to fall in here with supported content type detection as is

0 commit comments

Comments
 (0)