tailscale/ssh/tailssh/accept_env.go

120 lines
3.3 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailssh
import (
"fmt"
"slices"
"strings"
)
// filterEnv filters a passed in environ string slice (a slice with strings
// representing environment variables in the form "key=value") based on
// the supplied slice of acceptEnv values.
//
// acceptEnv is a slice of environment variable names that are allowlisted
// for the SSH rule in the policy file.
//
// acceptEnv values may contain * and ? wildcard characters which match against
// zero or one or more characters and a single character respectively.
func filterEnv(acceptEnv []string, environ []string) ([]string, error) {
var acceptedPairs []string
// Quick return if we have an empty list.
if acceptEnv == nil || len(acceptEnv) == 0 {
return acceptedPairs, nil
}
for _, envPair := range environ {
variableName, _, ok := strings.Cut(envPair, "=")
if !ok {
return nil, fmt.Errorf(`invalid environment variable: %q. Variables must be in "KEY=VALUE" format`, envPair)
}
// Short circuit if we have a direct match between the environment
// variable and an AcceptEnv value.
if slices.Contains(acceptEnv, variableName) {
acceptedPairs = append(acceptedPairs, envPair)
continue
}
// Otherwise check if we have a wildcard pattern that matches.
if matchAcceptEnv(acceptEnv, variableName) {
acceptedPairs = append(acceptedPairs, envPair)
continue
}
}
return acceptedPairs, nil
}
// matchAcceptEnv is a convenience function that wraps calling matchAcceptEnvPattern
// with every value in acceptEnv for a given env that is being matched against.
func matchAcceptEnv(acceptEnv []string, env string) bool {
for _, pattern := range acceptEnv {
if matchAcceptEnvPattern(pattern, env) {
return true
}
}
return false
}
// matchAcceptEnvPattern returns true if the pattern matches against the target string.
// Patterns may include * and ? wildcard characters which match against zero or one or
// more characters and a single character respectively.
func matchAcceptEnvPattern(pattern string, target string) bool {
patternIdx := 0
targetIdx := 0
for {
// If we are at the end of the pattern we can only have a match if we
// are also at the end of the target.
if patternIdx >= len(pattern) {
return targetIdx >= len(target)
}
if pattern[patternIdx] == '*' {
// Optimization to skip through any repeated asterisks as they
// have the same net effect on our search.
for patternIdx < len(pattern) {
if pattern[patternIdx] != '*' {
break
}
patternIdx++
}
// We are at the end of the pattern after matching the asterisk,
// implying a match.
if patternIdx >= len(pattern) {
return true
}
// Search through the target sequentially for the next character
// from the pattern string, recursing into matchAcceptEnvPattern
// to try and find a match.
for ; targetIdx < len(target); targetIdx++ {
if matchAcceptEnvPattern(pattern[patternIdx:], target[targetIdx:]) {
return true
}
}
// No match after searching through the entire target.
return false
}
if targetIdx >= len(target) {
return false
}
if pattern[patternIdx] != '?' && pattern[patternIdx] != target[targetIdx] {
return false
}
patternIdx++
targetIdx++
}
}