Documentation
¶
Overview ¶
Package sanitize makes attacker-influenced tailnet text safe to show (spec §3, §17).
Struct fields carrying such text are tagged `tk:"untrusted"` (a comma-separated tag; other values such as "rows" and "col" are used by the CLI renderer).
Index ¶
- Constants
- func EmailPaths(t reflect.Type) []string
- func Fields(ptr any, max int)
- func HasTag(tag reflect.StructTag, option string) bool
- func IsTaggedDevices(s string) bool
- func Mask(ptr any)
- func MaskAddrs(s string) string
- func MaskEmails(s string) string
- func MaskID(id string) string
- func MaskIdentifier(s string) string
- func MaskText(s string) string
- func PersonalPaths(t reflect.Type) []string
- func PlainPaths(t reflect.Type) []string
- func String(s string, max int) string
- func Text(s string, max int) string
- func UntrustedPaths(t reflect.Type) []string
- type Masker
- func (m Masker) EmailRefs(s string) []string
- func (m Masker) Emails(s string) string
- func (m Masker) Identifier(s string) string
- func (m Masker) IdentifierRefs(s string, max int) []string
- func (m Masker) Mask(ptr any)
- func (m Masker) Remask(s string) string
- func (m Masker) Text(s string) string
- func (m Masker) TextRefs(s string, max int) []string
Constants ¶
const DefaultMax = 1024
DefaultMax is the byte cap for one untrusted string in tool results.
const MaxText = 16 << 20
MaxText caps one multi-line field (`tk:"untrusted,multiline"`, such as a page of the policy file). It is a backstop: acl_get pages the policy below 4 MiB of JSON text and refuses a longer line, so no page of it reaches this cap.
Variables ¶
This section is empty.
Functions ¶
func EmailPaths ¶
EmailPaths lists the fields tagged `tk:"emails"`: see PersonalPaths.
func Fields ¶
Fields escapes, in place, every string reachable from ptr (a pointer). Strings inside a field tagged `tk:"untrusted"` (text tailnet members choose) are also capped at max and have credential shapes replaced (tripwire.Redact); a tagged struct, slice or map field has everything inside it treated as untrusted. A string field tagged `tk:"untrusted,multiline"` keeps its line feeds and tabs (Text) and is capped at MaxText. Map keys and values are rebuilt; keys that are the same text once escaped and capped are all kept (see distinctKey). It panics unless ptr is a non-nil pointer: skipping the escaping of a result is a programming error that must not pass silently.
func IsTaggedDevices ¶
IsTaggedDevices reports whether s is the owner Tailscale reports for tagged devices, which names no person and is never masked.
func Mask ¶
func Mask(ptr any)
Mask rewrites, in place, the personal data reachable from ptr, without pseudonyms. See Masker.Mask.
func MaskAddrs ¶
MaskAddrs cuts every public IP address in s to its network. Masked text masks to itself.
An address is found at the end of a run of address characters, however the run begins: a label that ends in a hex digit or a letter ("ipv6:2001:db8::1", "eth0:2001:db8::/48"), a name and a full stop ("ipv6.203.0.113.7"), or an escape that ends in a digit ("%22203.0.113.7") all run into the address, and read as part of it they hide it. The run is read again from each place an address can start in it (see starts), so what the label is glued to does not matter.
A dotted number is read first for the addresses it holds anywhere (see maskWindows), since an address need not end it ("203.0.113.7.443"). Masking can make a new run of text: "123." and the "8.8.8.0/24" that "::ffff:8.8.8.8" is cut to read as the number 123.8.8.8.0/24, which holds an address again, and a network that masking wrote can be followed by the digits of the next address. So the text is masked again until nothing changes, at most maxRounds times.
func MaskEmails ¶
MaskEmails masks every email address in s: each "@" with a local part before it and a domain after it. "ask amy@example.com." becomes "ask a…@example.com.". A JSON escape such as \u0061 counts as one character of an address, and \u0040 as an "@": a policy file can spell a login that way, and Tailscale reads it as one. A chain such as a@b@c.d is masked whole, to its first character: masking a@b alone would leave b@c.d.
func MaskID ¶
MaskID keeps the first four characters of an identifier such as a tailnet or OAuth client ID and drops the rest, so a report shows which one is meant without spreading the whole ID into terminals and agent transcripts. IDs of eight characters or fewer are kept whole: too short to identify anything.
func MaskIdentifier ¶
MaskIdentifier keeps the first character of a login name, display name or email address, and the domain when the whole value is an address.
func PersonalPaths ¶
PersonalPaths lists, in sorted order, the JSON paths of fields tagged `tk:"personal"` in type t, and PlainPaths those tagged `tk:"plain"`. Both panic on a tag Mask cannot honour: personal on anything but a string, a pointer to one or a slice of them; plain on a field that is not untrusted; or both on one field. A tool with such a field fails at registration instead of returning the value unmasked.
func String ¶
String renders every rune that does not print visibly (control characters, ANSI escape introducers, bidirectional, zero-width and other format characters, tag characters, variation selectors, blank fillers) and invalid UTF-8 as visible \u{XXXX} escapes, then cuts the result to max bytes on a rune boundary. max <= 0 means no cap.
func Text ¶
Text is String for multi-line documents such as the policy file: line feeds and tabs are kept, a CRLF line end is shown as LF, and a lone carriage return, like every other invisible character, is escaped. A terminal only moves the cursor for LF and TAB; nothing else can redraw or hide text.
func UntrustedPaths ¶
UntrustedPaths lists, in sorted order, the JSON paths of fields tagged `tk:"untrusted"` in type t. Slice elements are written as "[]".
Types ¶
type Masker ¶
type Masker struct {
// contains filtered or unexported fields
}
Masker masks personal data. With a privacy key it also follows each masked identifier and email address with its pseudonym (spec Appendix C), so a model can tell values apart, and filter on them, without seeing them. The zero Masker masks without pseudonyms.
func NewMasker ¶
NewMasker returns a Masker that names masked values under k; a nil k names nothing.
func (Masker) EmailRefs ¶
EmailRefs returns, in order, the pseudonyms Emails appends to s: one for each address it masks, found by the same scan. A filter on a pseudonym matches exactly the text that shows it only if it asks this scan and not its own.
func (Masker) Emails ¶
Emails masks every email address in s, as MaskEmails does, and names each one when m has a key.
func (Masker) Identifier ¶
Identifier masks a login name, display name or email address, and appends its pseudonym when the value was masked. It is not idempotent on masked input: masking "a…@example.com [p:…]" again would name that text, so a personal field is masked once.
func (Masker) IdentifierRefs ¶
IdentifierRefs returns the pseudonym a personal field holding s shows once it has been through Mask and then Fields with the byte cap max (0 means none): none when masking leaves s unchanged (an empty value, or the owner of tagged devices) or when the cap cuts the pseudonym off. As for TextRefs, a filter on a pseudonym the display does not show would confirm a guess at the value.
func (Masker) Mask ¶
Mask rewrites, in place, the personal data reachable from ptr (a pointer), as described above. PersonalPaths, run at registration, guarantees that every field tagged personal is a string, a pointer to one or a slice of them.
func (Masker) Remask ¶
Remask is Text for text a tool already masked with Text: the same scan, so whatever escaping or cutting exposed since is masked too. It does not rewrite "[p:": every marker in text already through Text was appended by it, and the rewrite would destroy them. Only a field tagged premasked is masked this way.
func (Masker) Text ¶
Text masks the email addresses and public IP addresses in free text. With a key it first rewrites every "[p:" the text holds (see unforged), so a pseudonym marker in the result is one Text appended and nothing else.
func (Masker) TextRefs ¶
TextRefs returns the distinct pseudonyms, in order of first appearance, that an untrusted string shows once it has been through Mask and then Fields with the byte cap max (0 means none): those Text appends, less any the credential redaction or the cap cut off. A pseudonym that is not displayed must not match a filter, or a guess at a hidden value could be confirmed by asking for it.
Whether one survived is read off the displayed text. That is sound because Text rewrites every "[p:" the string holds before it names an address: a marker in the display is one Text appended, so what the display shows is exactly what it appended, less what the cap cut.