fix(auth): harden Windows storage bridge

This commit is contained in:
2026-08-16 22:16:13 +02:00
parent c9b02fc57e
commit 7d9ca13f1d
12 changed files with 473 additions and 29 deletions
+7 -2
View File
@@ -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 {