fix(auth): harden Windows storage bridge
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
package authstorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -60,7 +61,11 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.
|
||||
if err := ctx.Err(); err != nil {
|
||||
return fail(stderr)
|
||||
}
|
||||
decoder := json.NewDecoder(io.LimitReader(stdin, maximumProtocolBytes+1))
|
||||
payload, err := io.ReadAll(io.LimitReader(stdin, maximumProtocolBytes+1))
|
||||
if err != nil || len(payload) > maximumProtocolBytes {
|
||||
return fail(stderr)
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(payload))
|
||||
decoder.DisallowUnknownFields()
|
||||
var input request
|
||||
if err := decoder.Decode(&input); err != nil {
|
||||
@@ -229,7 +234,7 @@ func createPrivate(directory, filename string, contents []byte) (bool, error) {
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return false, errInvalid
|
||||
}
|
||||
if err := safeio.WriteCanonicalNewFile(path, contents, 0o600); err == nil {
|
||||
if err := safeio.WriteCanonicalNewPrivateFile(path, contents, 0o600); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if safeio.ValidatePrivateRegular(path) == nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@@ -43,6 +44,44 @@ func TestProtocolCreatesReadsReplacesListsAndRemovesPrivateRecord(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolListSerializesLowerCamelBridgeDTO(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
filename := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.json"
|
||||
runRequest(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("record"))})
|
||||
input, err := json.Marshal(request{Version: 1, Operation: "list", Root: root, Directory: "sessions"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run(context.Background(), nil, bytes.NewReader(input), &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("Run() code = %d stderr = %q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), `"name":"`+filename+`"`) || strings.Contains(stdout.String(), `"Name":`) {
|
||||
t.Fatalf("list bridge JSON = %q, want lower-camel entry fields", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRejectsProtocolOverflowAndTrailingJSONValues(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
valid, err := json.Marshal(request{Version: 1, Operation: "list", Root: root, Directory: "sessions"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(valid) >= maximumProtocolBytes {
|
||||
t.Fatal("test request unexpectedly consumes protocol bound")
|
||||
}
|
||||
|
||||
overflowWhitespace := append(append([]byte(nil), valid...), bytes.Repeat([]byte(" "), maximumProtocolBytes+1-len(valid))...)
|
||||
runRawRejected(t, overflowWhitespace)
|
||||
|
||||
withinBound := append(append([]byte(nil), valid...), []byte("{}")...)
|
||||
runRawRejected(t, withinBound)
|
||||
|
||||
crossingBound := append(append([]byte(nil), valid...), bytes.Repeat([]byte(" "), maximumProtocolBytes-len(valid)-1)...)
|
||||
crossingBound = append(crossingBound, []byte("{}")...)
|
||||
runRawRejected(t, crossingBound)
|
||||
}
|
||||
|
||||
func TestProtocolClaimConsumeIsAtomicAcrossConcurrentRequests(t *testing.T) {
|
||||
root := filepath.Join(privateTestRoot(t), "auth")
|
||||
filename := "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.json"
|
||||
@@ -140,6 +179,14 @@ func runRejected(t *testing.T, value request) {
|
||||
}
|
||||
}
|
||||
|
||||
func runRawRejected(t *testing.T, input []byte) {
|
||||
t.Helper()
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run(context.Background(), nil, bytes.NewReader(input), &stdout, &stderr); code == 0 || stdout.Len() != 0 || stderr.String() != "tht: auth storage request failed\n" {
|
||||
t.Fatalf("Run() rejection code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func decodeContent(t *testing.T, value response) string {
|
||||
t.Helper()
|
||||
contents, err := base64.StdEncoding.DecodeString(value.ContentBase64)
|
||||
|
||||
@@ -45,6 +45,16 @@ func TestProtocolRejectsPermissiveDACLAndReparseRoot(t *testing.T) {
|
||||
runRejected(t, request{Version: 1, Operation: "create", Root: linkedRoot, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("private"))})
|
||||
}
|
||||
|
||||
func TestProtocolRejectsRecordCreationAfterSessionsDirectoryDACLBecomesPermissive(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "auth")
|
||||
filename := "abababababababababababababababababababababababababababababababab.json"
|
||||
runRequest(t, request{Version: 1, Operation: "list", Root: root, Directory: "sessions"})
|
||||
if err := setPermissiveDACL(filepath.Join(root, "sessions")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runRejected(t, request{Version: 1, Operation: "create", Root: root, Directory: "sessions", Filename: filename, ContentBase64: base64.StdEncoding.EncodeToString([]byte("record"))})
|
||||
}
|
||||
|
||||
func setPermissiveDACL(path string) error {
|
||||
world, err := windows.StringToSid("S-1-1-0")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user