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, limit int)
- func HasTag(tag reflect.StructTag, option string) bool
- func IsTaggedDevices(s string) bool
- func LocalByte(c byte) 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, limit int) string
- func Text(s string, limit 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, limit int) []string
- func (m Masker) Mask(ptr any)
- func (m Masker) PolicyText(s string) string
- func (m Masker) Remask(s string) string
- func (m Masker) Text(s string) string
- func (m Masker) TextRefs(s string, limit int) []string
- func (m Masker) Unforge(s string) 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 limit 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 LocalByte ¶ added in v0.6.0
LocalByte reports whether c, taken alone, is a byte of an email address's local part, as the masker reads a byte. It is a byte test and nothing more: the escapes the masker also reads in a local part (a percent-encoded byte, a JSON or brace escape) and a quoted word span more than one byte, and LocalByte sees none of them. A caller that masks text around an address uses it to find where the address begins, and reads those spellings itself.
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 limit bytes on a rune boundary. limit <= 0 means no cap. A limit under the 14 bytes of the notice that marks a cut gets the text cut to it, without the notice: the result is never longer than the limit.
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 limit (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) PolicyText ¶ added in v0.6.0
PolicyText masks the email addresses in text taken from a policy, and nothing else: hosts, CIDRs, tags and groups are the rules and stay whole. With a key it first rewrites every "[p:" the text holds, as Text does, so that a marker in the result is one PolicyText appended and nothing else: a comment or a name in the policy can spell one, and must not pass for the masker's. A second pass over its result with Emails, to catch what escaping exposed, leaves those markers alone.
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 limit (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.
func (Masker) Unforge ¶ added in v0.6.0
Unforge rewrites every "[p:" in s, as Text and PolicyText do first, and returns s as it came when the masker has no key. It is for text that this masker does not mask but that must not carry a marker the masker did not append: a message Tailscale echoed from a proposal, which the registry's emails pass would otherwise read as a pseudonym.