-
Notifications
You must be signed in to change notification settings - Fork 0
/
safesql.go
93 lines (81 loc) · 3.12 KB
/
safesql.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Package safesql implements a safe version of database/sql to prevent accidental SQL injections.
//
// Usage should ideally be identical to the standard sql package with the exception that strings
// should be String instead.
//
// The simplest way to transition to this package is to turn statements like
//
// db.Query("SELECT ...", args...)
//
// into
//
// db.Query(safesql.New("SELECT ..."), args...)
//
// Once safesql is adopted, importing database/sql should be banned with the sole
// exception of this package.
// Types from database/sql that are inherently safe have aliases in this package to
// allow for an easier transition and smaller allowlists.
//
// For leftover exceptions and for transitions the legacyconversions and uncheckedconversions packages can be used.
// Similarly, the testconversions should only be used during tests.
package safesql
import (
"fmt"
"strings"
"github.com/empijei/safesql/internal/raw"
"golang.org/x/exp/constraints"
)
func init() {
// Initialize the bypass mechanisms for unchecked and legacy conversions.
raw.StringCtor = func(unsafe string) String { return String{unsafe} }
}
type stringConstant string
// String wraps a string that is safe and does not contain user-controlled input.
type String struct {
s string
}
// New constructs a String from a compile-time constant string.
// Since the stringConstant type is unexported the only way to call this function
// outside of this package is to pass a string literal or an untyped string const.
//
// Note(empijei): this can be bypassed by using generics with ~string, but that
// feels very unlikely to happen by accident and malicious programmers are not
// part of the threat model of this package.
func New(text stringConstant) String { return String{string(text)} }
type RealNumber interface {
constraints.Integer | constraints.Float
}
// NewFromNumber constructs a String from a number.
func NewFromNumber[N RealNumber](i N) String { return String{fmt.Sprintf("%v", i)} }
// StringConcat concatenates the given [String]s into a trusted string.
//
// Note(empijei): this function may be abused to create arbitrary queries from
// user inputs, but malicious programmers are not part of the threat model of this package.
func StringConcat(ss ...String) String {
return StringJoin(ss, String{})
}
// StringJoin joins the given [String]s with the given separator the same way strings.Join would.
//
// Note(empijei): this function may be abused to create arbitrary queries from
// user inputs, but malicious programmers are not part of the threat model of this package.
func StringJoin(ss []String, sep String) String {
accum := make([]string, 0, len(ss))
for _, s := range ss {
accum = append(accum, s.s)
}
return String{strings.Join(accum, sep.s)}
}
// String returns the internal representation of the [String], safe to be used
// with a sql engine.
func (t String) String() string {
return t.s
}
// StringSplit functions as strings.Split but for [String]s.
func StringSplit(s String, sep string) []String {
spl := strings.Split(s.s, sep)
accum := make([]String, 0, len(spl))
for _, s := range spl {
accum = append(accum, String{s})
}
return accum
}