174 lines
5.5 KiB
Go
174 lines
5.5 KiB
Go
//go:build !windows
|
|
|
|
package safeio
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func lockCanonicalPrivateClaimOperation() func() {
|
|
return func() {}
|
|
}
|
|
|
|
func claimCanonicalPrivateRegular(source, claim string) (bool, error) {
|
|
directory, sourceName, err := openCanonicalParentDirectory(source)
|
|
if err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
defer unix.Close(directory)
|
|
claimName := claim[len(claim)-len(claimBaseName(claim)):]
|
|
if err := requirePrivateRegularAtLinks(directory, sourceName, 1); err != nil {
|
|
if requireSamePrivatePairAt(directory, sourceName, claimName) == nil {
|
|
return false, nil
|
|
}
|
|
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
|
|
return false, nil
|
|
}
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Linkat(directory, sourceName, directory, claimName, 0); err != nil {
|
|
if errors.Is(err, unix.EEXIST) && privateRegularAtAllowedLinks(directory, claimName, 1, 2) == nil {
|
|
return false, nil
|
|
}
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Fsync(directory); err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func readCanonicalPrivateClaim(source, claim string, maximum int64) ([]byte, bool, error) {
|
|
directory, sourceName, err := openCanonicalParentDirectory(source)
|
|
if err != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
defer unix.Close(directory)
|
|
claimName := claim[len(claim)-len(claimBaseName(claim)):]
|
|
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
|
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
|
|
return nil, false, nil
|
|
}
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
descriptor, err := unix.Openat(directory, sourceName, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
|
if err != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
file := os.NewFile(uintptr(descriptor), "tht-safeio-oidc-claim")
|
|
if file == nil {
|
|
unix.Close(descriptor)
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
defer file.Close()
|
|
var before unix.Stat_t
|
|
if err := unix.Fstat(descriptor, &before); err != nil || !privateRegularStatWithLinks(before, 2) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
contents, err := io.ReadAll(io.LimitReader(file, maximum+1))
|
|
if err != nil || int64(len(contents)) > maximum {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
var after unix.Stat_t
|
|
if err := unix.Fstat(descriptor, &after); err != nil || !sameUnixPrivateFile(before, after) || !privateRegularStatWithLinks(after, 2) {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
|
return nil, false, ErrUnsafeFile
|
|
}
|
|
return contents, true, nil
|
|
}
|
|
|
|
func removeCanonicalPrivateClaim(source, claim string) (bool, error) {
|
|
directory, sourceName, err := openCanonicalParentDirectory(source)
|
|
if err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
defer unix.Close(directory)
|
|
claimName := claim[len(claim)-len(claimBaseName(claim)):]
|
|
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
|
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
|
|
return false, nil
|
|
}
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Unlinkat(directory, sourceName, 0); err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := requirePrivateRegularAtLinks(directory, claimName, 1); err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Unlinkat(directory, claimName, 0); err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
if err := unix.Fsync(directory); err != nil {
|
|
return false, ErrUnsafeFile
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func claimBaseName(path string) string {
|
|
for index := len(path) - 1; index >= 0; index-- {
|
|
if path[index] == byte(os.PathSeparator) {
|
|
return path[index+1:]
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
|
|
func privateRegularAtAllowedLinks(directory int, name string, allowed ...uint64) error {
|
|
var stat unix.Stat_t
|
|
if err := unix.Fstatat(directory, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
for _, links := range allowed {
|
|
if privateRegularStatWithLinks(stat, links) {
|
|
return nil
|
|
}
|
|
}
|
|
return ErrUnsafeFile
|
|
}
|
|
|
|
func requirePrivateRegularAtLinks(directory int, name string, links uint64) error {
|
|
return privateRegularAtAllowedLinks(directory, name, links)
|
|
}
|
|
|
|
func requireSamePrivatePairAt(directory int, source, claim string) error {
|
|
var left, right unix.Stat_t
|
|
if err := unix.Fstatat(directory, source, &left, unix.AT_SYMLINK_NOFOLLOW); err != nil || !privateRegularStatWithLinks(left, 2) {
|
|
return ErrUnsafeFile
|
|
}
|
|
if err := unix.Fstatat(directory, claim, &right, unix.AT_SYMLINK_NOFOLLOW); err != nil || !privateRegularStatWithLinks(right, 2) {
|
|
return ErrUnsafeFile
|
|
}
|
|
if !sameUnixPrivateFile(left, right) {
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isPrivateClaimAbsentOrOrphanAt(directory int, source, claim string) bool {
|
|
var sourceStat, claimStat unix.Stat_t
|
|
sourceErr := unix.Fstatat(directory, source, &sourceStat, unix.AT_SYMLINK_NOFOLLOW)
|
|
claimErr := unix.Fstatat(directory, claim, &claimStat, unix.AT_SYMLINK_NOFOLLOW)
|
|
if errors.Is(sourceErr, unix.ENOENT) && errors.Is(claimErr, unix.ENOENT) {
|
|
return true
|
|
}
|
|
return errors.Is(sourceErr, unix.ENOENT) && claimErr == nil && privateRegularStatWithLinks(claimStat, 1)
|
|
}
|
|
|
|
func privateRegularStatWithLinks(stat unix.Stat_t, links uint64) bool {
|
|
return stat.Mode&unix.S_IFMT == unix.S_IFREG && uint64(stat.Nlink) == links && stat.Mode&0o7777 == 0o600
|
|
}
|
|
|
|
func sameUnixPrivateFile(left, right unix.Stat_t) bool {
|
|
return left.Dev == right.Dev && left.Ino == right.Ino && left.Size == right.Size && left.Mtim == right.Mtim
|
|
}
|