mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-30 15:38:49 +02:00
76 lines
3.1 KiB
Go
76 lines
3.1 KiB
Go
// Package lure builds and resolves the short recipient identifiers that appear
|
|
// in a lure URL, for example https://example.com/account/4H7K9QM2XR3T.
|
|
//
|
|
// A code is stored and matched byte for byte. Nothing is folded, uppercased or
|
|
// stripped, so an operator who picks Special-42 gets exactly that link and it
|
|
// stays distinct from special-42. This is required by base58, whose two cases
|
|
// are different symbols, and applying the same rule everywhere keeps one
|
|
// matching behaviour rather than one per algorithm.
|
|
package lure
|
|
|
|
import "strings"
|
|
|
|
// Alphabet is the Crockford base32 encoding alphabet. It omits I, L, O and U
|
|
// so that a code read aloud or transcribed from a message cannot be confused
|
|
// with a visually similar character or spell an unintended word.
|
|
const Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
|
|
// Base58Alphabet is the Bitcoin base58 alphabet. It keeps both cases, which
|
|
// makes a code shorter for the same number of combinations, and drops the four
|
|
// glyphs that are hard to tell apart in print: 0, O, I and l.
|
|
const Base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
|
|
// MinLength and MaxLength bound the operator selectable length of a *generated*
|
|
// code. The lower bound is a deliberate operator choice for short lived
|
|
// campaigns where a guessable link is acceptable.
|
|
const (
|
|
MinLength = 6
|
|
MaxLength = 16
|
|
DefaultLength = 12
|
|
)
|
|
|
|
// MaxCustomLength is the only limit placed on an operator written code, and it
|
|
// exists because the column is varchar(64).
|
|
const MaxCustomLength = 64
|
|
|
|
// IsCandidate reports whether a path segment or query value could be a lure
|
|
// code and is therefore worth a database probe.
|
|
//
|
|
// Because an operator written code may be any text, this cannot check an
|
|
// alphabet. It only rules out what could not be a code at all: anything too
|
|
// long for the column, and anything that would have to be escaped to sit in a
|
|
// single path segment. A dot is allowed, so a lure can end in something like
|
|
// invoice.pdf, at the cost of one indexed probe on static asset requests.
|
|
func IsCandidate(s string) bool {
|
|
if s == "" || len(s) > MaxCustomLength {
|
|
return false
|
|
}
|
|
// structural URL characters, which would split the segment or need escaping
|
|
if strings.ContainsAny(s, "/%?#\\") {
|
|
return false
|
|
}
|
|
// quotes and angle brackets, because the finished URL is written into a
|
|
// mail body rendered with text/template, which does not escape it
|
|
if strings.ContainsAny(s, "\"'<>") {
|
|
return false
|
|
}
|
|
// control characters and whitespace. a code containing one could never be
|
|
// matched back from a request path, so it would be silently unreachable
|
|
for _, r := range s {
|
|
if r <= 0x20 || r == 0x7f {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IsValidCustom reports whether an operator supplied code can be stored.
|
|
//
|
|
// The code is kept byte for byte as written, so the only rules are the ones
|
|
// that would otherwise make it unreachable: it has to fit the column and it has
|
|
// to survive as a single path segment without escaping. A path with more than
|
|
// one segment comes from the template URL path, not from the code.
|
|
func IsValidCustom(s string) bool {
|
|
return IsCandidate(s)
|
|
}
|