Files
brianmcgee 923b96db2e state: validate file names in Put/PutTombstone
`files.Put()` and `files.PutTombstone()` accept arbitrary `sig.Name` values without validation. This allows `../` paths to resolve outside the state directory.

The NixOS module/systemd service would have prevented this from being a real problem, but best to lock it down all the same.
2026-04-08 12:21:25 +01:00

163 lines
4.0 KiB
Go

package model_test
import (
"testing"
"git.clan.lol/clan/data-mesher/pkg/crypto"
"git.clan.lol/clan/data-mesher/pkg/model"
"git.clan.lol/clan/data-mesher/test"
"github.com/stretchr/testify/require"
)
func TestValidateName_Valid(t *testing.T) {
t.Parallel()
as := require.New(t)
// static file names
as.NoError(model.ValidateName("foo"))
as.NoError(model.ValidateName("foo/bar"))
as.NoError(model.ValidateName("ns/abc123"))
as.NoError(model.ValidateName("a"))
as.NoError(model.ValidateName("with_underscore"))
as.NoError(model.ValidateName("nested/path/file"))
// namespace paths (namespace/url_encoded_key)
keys := test.GenerateKeys(t, 1)
as.NoError(model.ValidateName("myns/" + keys[0].Public.URLEncoded()))
}
func TestValidateName_Invalid(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
}{
// empty
{"empty string", ""},
// null bytes (truncate paths at OS syscall level)
{"null byte mid-string", "foo\x00bar"},
{"null byte as traversal smuggle", "valid\x00../../etc/passwd"},
{"leading null byte", "\x00foo"},
{"trailing null byte", "foo\x00"},
// path traversal
{"parent traversal", "../../etc/passwd"},
{"mid-path traversal", "foo/../bar"},
{"bare dotdot", ".."},
{"trailing dotdot", "foo/.."},
{"leading dotdot", "../foo"},
// dot as path element (fs.ValidPath rejects)
{"bare dot", "."},
{"dot element mid-path", "foo/./bar"},
// absolute paths
{"absolute path", "/etc/passwd"},
// empty path segments / structural issues (fs.ValidPath rejects)
{"trailing slash", "foo/"},
{"leading slash", "/foo"},
{"double slash", "foo//bar"},
// invalid UTF-8
{"invalid utf8", "foo\xfe\xff"},
// control characters and whitespace
{"space in name", "foo bar"},
{"newline in name", "foo\nbar"},
{"tab in name", "foo\tbar"},
// characters not in NameRegex and not valid namespace paths
{"dot extension", "foo.txt"},
{"hyphen", "foo-bar"},
{"uppercase", "FOO"},
{"backslash", "foo\\bar"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.ErrorIs(t, model.ValidateName(tc.input), model.ErrInvalidName)
})
}
}
func TestValidateNamespaceName_Valid(t *testing.T) {
t.Parallel()
as := require.New(t)
as.NoError(model.ValidateNamespaceName("dns"))
as.NoError(model.ValidateNamespaceName("my_namespace"))
as.NoError(model.ValidateNamespaceName("ns123"))
}
func TestValidateNamespaceName_Invalid(t *testing.T) {
t.Parallel()
as := require.New(t)
as.Error(model.ValidateNamespaceName("DNS")) // uppercase
as.Error(model.ValidateNamespaceName("my-ns")) // hyphen
as.Error(model.ValidateNamespaceName("a/b")) // path separator
as.Error(model.ValidateNamespaceName("")) // empty
as.Error(model.ValidateNamespaceName("has space")) // space
}
func TestParseNamespacePath_Valid(t *testing.T) {
t.Parallel()
as := require.New(t)
keys := test.GenerateKeys(t, 1)
encoded := keys[0].Public.URLEncoded()
name := "myns/" + encoded
namespace, signerKey, err := model.ParseNamespacePath(name)
as.NoError(err)
as.Equal("myns", namespace)
as.True(signerKey.Equal(keys[0].Public))
}
func TestParseNamespacePath_NoSlash(t *testing.T) {
t.Parallel()
as := require.New(t)
_, _, err := model.ParseNamespacePath("noslash")
as.Error(err)
as.Contains(err.Error(), "not a namespace path")
}
func TestParseNamespacePath_InvalidKey(t *testing.T) {
t.Parallel()
as := require.New(t)
_, _, err := model.ParseNamespacePath("ns/notavalidkey")
as.Error(err)
as.Contains(err.Error(), "invalid signer key")
}
func TestParseNamespacePath_RoundTrip(t *testing.T) {
t.Parallel()
as := require.New(t)
// generate several keys and verify round-trip through URL encoding
keys := test.GenerateKeys(t, 5)
for _, key := range keys {
name := "test_ns/" + key.Public.URLEncoded()
_, parsed, err := model.ParseNamespacePath(name)
as.NoError(err)
as.True(parsed.Equal(key.Public))
// also verify the parsed key can be used with crypto functions
_, err = crypto.ParsePublicKeyURLEncoded(key.Public.URLEncoded())
as.NoError(err)
}
}