diff --git a/cmd/tailscale/cli/file.go b/cmd/tailscale/cli/file.go index 4b29ddbc7..6986f58ca 100644 --- a/cmd/tailscale/cli/file.go +++ b/cmd/tailscale/cli/file.go @@ -29,6 +29,7 @@ import ( "tailscale.com/ipn" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" + "tailscale.com/util/quarantine" "tailscale.com/version" ) @@ -393,6 +394,10 @@ func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targe if err != nil { return "", 0, err } + // Apply quarantine attribute before copying + if err := quarantine.SetOnFile(f); err != nil { + return "", 0, fmt.Errorf("failed to apply quarantine attribute to file %v: %v", f.Name(), err) + } _, err = io.Copy(f, rc) if err != nil { f.Close() diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 3fa91bafc..27927ec79 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -7,6 +7,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy github.com/fxamacker/cbor/v2 from tailscale.com/tka github.com/golang/groupcache/lru from tailscale.com/net/dnscache + D github.com/google/uuid from tailscale.com/util/quarantine github.com/hdevalence/ed25519consensus from tailscale.com/tka L github.com/josharian/native from github.com/mdlayher/netlink+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces @@ -105,6 +106,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/util/lineread from tailscale.com/net/interfaces+ tailscale.com/util/mak from tailscale.com/net/netcheck+ tailscale.com/util/multierr from tailscale.com/control/controlhttp + tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli tailscale.com/util/singleflight from tailscale.com/net/dnscache L tailscale.com/util/strs from tailscale.com/hostinfo W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+ @@ -176,6 +178,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep crypto/tls from github.com/tcnksm/go-httpstat+ crypto/x509 from crypto/tls+ crypto/x509/pkix from crypto/x509+ + D database/sql/driver from github.com/google/uuid embed from tailscale.com/cmd/tailscale/cli+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ diff --git a/util/quarantine/quarantine.go b/util/quarantine/quarantine.go new file mode 100644 index 000000000..e8f404dfb --- /dev/null +++ b/util/quarantine/quarantine.go @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package quarantine sets platform specific "quarantine" attributes on files +// that are received from other hosts. +package quarantine + +import "os" + +// SetOnFile sets the platform-specific quarantine attribute (if any) on the +// provided file. +func SetOnFile(f *os.File) error { + return setQuarantineAttr(f) +} diff --git a/util/quarantine/quarantine_darwin.go b/util/quarantine/quarantine_darwin.go new file mode 100644 index 000000000..69ccc0b30 --- /dev/null +++ b/util/quarantine/quarantine_darwin.go @@ -0,0 +1,57 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package quarantine + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/google/uuid" + "golang.org/x/sys/unix" +) + +func setQuarantineAttr(f *os.File) error { + sc, err := f.SyscallConn() + if err != nil { + return err + } + + now := time.Now() + + // We uppercase the UUID to match what other applications on macOS do + id := strings.ToUpper(uuid.New().String()) + + // kLSQuarantineTypeOtherDownload; this matches what AirDrop sets when + // receiving a file. + quarantineType := "0001" + + // This format is under-documented, but the following links contain a + // reasonably comprehensive overview: + // https://eclecticlight.co/2020/10/29/quarantine-and-the-quarantine-flag/ + // https://nixhacker.com/security-protection-in-macos-1/ + // https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html + attrData := fmt.Sprintf("%s;%x;%s;%s", + quarantineType, // quarantine value + now.Unix(), // time in hex + "Tailscale", // application + id, // UUID + ) + + var innerErr error + err = sc.Control(func(fd uintptr) { + innerErr = unix.Fsetxattr( + int(fd), + "com.apple.quarantine", // attr + []byte(attrData), + 0, + ) + }) + if err != nil { + return err + } + return innerErr +} diff --git a/util/quarantine/quarantine_default.go b/util/quarantine/quarantine_default.go new file mode 100644 index 000000000..86c56845d --- /dev/null +++ b/util/quarantine/quarantine_default.go @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !darwin && !windows + +package quarantine + +import ( + "os" +) + +func setQuarantineAttr(f *os.File) error { + return nil +} diff --git a/util/quarantine/quarantine_windows.go b/util/quarantine/quarantine_windows.go new file mode 100644 index 000000000..ebf4498ec --- /dev/null +++ b/util/quarantine/quarantine_windows.go @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package quarantine + +import ( + "os" + "strings" +) + +func setQuarantineAttr(f *os.File) error { + // Documentation on this can be found here: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6e3f7352-d11c-4d76-8c39-2516a9df36e8 + // + // Additional information can be found at: + // https://www.digital-detective.net/forensic-analysis-of-zone-identifier-stream/ + // https://bugzilla.mozilla.org/show_bug.cgi?id=1433179 + content := strings.Join([]string{ + "[ZoneTransfer]", + + // "URLZONE_INTERNET" + // https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms537175(v=vs.85) + "ZoneId=3", + + // TODO(andrew): should/could we add ReferrerUrl or HostUrl? + }, "\r\n") + + return os.WriteFile(f.Name()+":Zone.Identifier", []byte(content), 0) +}