Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

validate.gno

5.54 Kb · 193 lines
  1package gnopm
  2
  3import "strings"
  4
  5// Size caps. Every string the chain stores is bounded: an unbounded field is an
  6// unbounded storage deposit, and on gno.land the deposit is paid per byte by
  7// whoever writes it.
  8const (
  9	MaxPkgPathLen = 256
 10	MaxRepoLen    = 512
 11	MaxDirLen     = 256
 12	MaxRefLen     = 255 // git's own limit for a single ref name
 13
 14	// MaxClaimants bounds how many addresses may claim one package path.
 15	// Unbounded, it is a spam surface: one path with ten thousand claims is a
 16	// Render that never returns and a listing nobody can read.
 17	MaxClaimants = 16
 18)
 19
 20// ValidCommit reports whether s is a git object id: 40 (SHA-1) or 64 (SHA-256)
 21// lowercase hex characters. Case is fixed so the same object always produces
 22// the same stored bytes, and so a verifier comparing two claims compares two
 23// comparable strings.
 24func ValidCommit(s string) bool {
 25	if len(s) != 40 && len(s) != 64 {
 26		return false
 27	}
 28	for i := 0; i < len(s); i++ {
 29		c := s[i]
 30		if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
 31			return false
 32		}
 33	}
 34	return true
 35}
 36
 37// ValidRepo reports whether s is a repository URL this registry accepts.
 38//
 39// Deliberately narrow: "https://" only, a host, and a path. Not a taste
 40// judgement about git transports, a safety one. Whatever is stored here is
 41// eventually rendered as a link by this realm, by gnoweb and by every explorer
 42// that reads the registry, and the set of schemes that are safe to hand a
 43// browser is exactly one. "javascript:", "data:" and "file:" are the attack;
 44// "git://" and "ssh://" are merely unreachable from a web page, and a claimant
 45// who needs one can point at the https mirror every forge already serves.
 46func ValidRepo(s string) bool {
 47	if s == "" || len(s) > MaxRepoLen {
 48		return false
 49	}
 50	const scheme = "https://"
 51	if !strings.HasPrefix(s, scheme) {
 52		return false
 53	}
 54	rest := s[len(scheme):]
 55	if rest == "" || strings.HasPrefix(rest, "/") {
 56		return false
 57	}
 58	// A host must be present and must not be a userinfo trick
 59	// ("https://[email protected]/x" fetches from evil.example while
 60	// reading as GitHub).
 61	slash := strings.IndexByte(rest, '/')
 62	host := rest
 63	if slash >= 0 {
 64		host = rest[:slash]
 65	}
 66	if host == "" || strings.ContainsAny(host, "@:") {
 67		return false
 68	}
 69	if !strings.Contains(host, ".") {
 70		return false
 71	}
 72	for i := 0; i < len(s); i++ {
 73		if !urlByteOK(s[i]) {
 74			return false
 75		}
 76	}
 77	return !strings.Contains(s, "..")
 78}
 79
 80// urlByteOK is an allowlist, not a denylist: the printable ASCII that appears
 81// in a real repository URL. Everything else, control characters and the bidi
 82// overrides in particular, is refused rather than escaped, because a field that
 83// can only hold safe bytes needs no escaping at any of its render sites.
 84func urlByteOK(c byte) bool {
 85	switch {
 86	case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
 87		return true
 88	}
 89	switch c {
 90	case '-', '.', '_', '~', ':', '/', '%', '+':
 91		return true
 92	}
 93	return false
 94}
 95
 96// ValidDir reports whether s names the subdirectory of the repository holding
 97// the package. Empty means the repository root, which is the common case for a
 98// single-package repo and must stay expressible.
 99func ValidDir(s string) bool {
100	if s == "" {
101		return true
102	}
103	if len(s) > MaxDirLen {
104		return false
105	}
106	if strings.HasPrefix(s, "/") || strings.HasSuffix(s, "/") {
107		return false
108	}
109	for _, p := range strings.Split(s, "/") {
110		if p == "" || p == "." || p == ".." {
111			return false
112		}
113		for i := 0; i < len(p); i++ {
114			if !pathByteOK(p[i]) {
115				return false
116			}
117		}
118	}
119	return true
120}
121
122func pathByteOK(c byte) bool {
123	switch {
124	case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
125		return true
126	}
127	switch c {
128	case '-', '.', '_':
129		return true
130	}
131	return false
132}
133
134// ValidRef reports whether s is a fully-qualified git ref, or empty.
135//
136// The ref is optional and is never the thing that is verified: a ref moves, a
137// commit does not. It is recorded so a reader can tell a claim pinned to a
138// released tag from one pinned to a commit on nobody's branch, and so a
139// verifier can check the claimed commit is still reachable from it rather than
140// dangling behind a force-push.
141func ValidRef(s string) bool {
142	if s == "" {
143		return true
144	}
145	if len(s) > MaxRefLen || !strings.HasPrefix(s, "refs/") {
146		return false
147	}
148	if strings.Contains(s, "..") || strings.Contains(s, "@{") {
149		return false
150	}
151	if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") {
152		return false
153	}
154	parts := strings.Split(s, "/")
155	if len(parts) < 2 {
156		return false
157	}
158	for _, p := range parts {
159		if p == "" || p == "@" || strings.HasPrefix(p, ".") || strings.HasSuffix(p, ".lock") {
160			return false
161		}
162		for i := 0; i < len(p); i++ {
163			if !refByteOK(p[i]) {
164				return false
165			}
166		}
167	}
168	return true
169}
170
171// refByteOK is an allowlist, and deliberately narrower than git's own rule.
172//
173// git rejects a handful of metacharacters and permits everything else, which
174// leaves a ref free to contain a backtick, a pipe or a bracket. Every other
175// field here is an allowlist precisely so that no consumer has to escape
176// anything, and one denylist field would undo that for all of them: a ref
177// carrying a pipe breaks out of a markdown table cell, and one carrying a
178// backtick breaks out of the inline-code span a renderer wraps it in.
179//
180// The cost is refs nobody writes. What stays expressible is every ref anyone
181// actually has: alphanumerics, and the four separators git tooling puts in a
182// name.
183func refByteOK(c byte) bool {
184	switch {
185	case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
186		return true
187	}
188	switch c {
189	case '-', '.', '_', '/', '+':
190		return true
191	}
192	return false
193}