mirror of
https://github.com/netauth/ldap.git
synced 2026-11-03 11:43:44 -06:00
internal/ldap: Quote search values to prevent query injection
This commit is contained in:
parent
ca0e2434c7
commit
efb4904910
|
|
@ -54,5 +54,20 @@ func mapToBleveStringQuery(attr, op, val string) (string, error) {
|
|||
return "", errors.New("search comparison is unsupported")
|
||||
}
|
||||
|
||||
return predicate + operator + val, nil
|
||||
return predicate + operator + quoteBleveValue(val), nil
|
||||
}
|
||||
|
||||
// quoteBleveValue makes a value safe to embed in a Bleve query string.
|
||||
//
|
||||
// Assertion values come straight from the network and Bleve parses its
|
||||
// queries, so an unquoted value such as "x Name:y" would add terms the
|
||||
// client never asked for. Quoting keeps the value as a single term, and
|
||||
// the wildcard used for attribute-present filters stays outside the
|
||||
// quotes so that it keeps its meaning.
|
||||
func quoteBleveValue(val string) string {
|
||||
if val == "*" {
|
||||
return val
|
||||
}
|
||||
escaped := strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(val)
|
||||
return `"` + escaped + `"`
|
||||
}
|
||||
|
|
|
|||
85
internal/ldap/bleve_mapper_test.go
Normal file
85
internal/ldap/bleve_mapper_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ps78674/goldap/message"
|
||||
)
|
||||
|
||||
func TestBuildBleveQuery(t *testing.T) {
|
||||
s := &server{l: testLogger()}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
filter message.Filter
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "equality on uid",
|
||||
filter: berEqualityFilter(t, "uid", "jsmith"),
|
||||
want: `ID:"jsmith"`,
|
||||
},
|
||||
{
|
||||
name: "equality on cn",
|
||||
filter: berEqualityFilter(t, "cn", "admins"),
|
||||
want: `Name:"admins"`,
|
||||
},
|
||||
{
|
||||
name: "attribute present",
|
||||
filter: berPresentFilter(t, "uid"),
|
||||
want: "ID:*",
|
||||
},
|
||||
{
|
||||
name: "or of two matches",
|
||||
filter: berOrFilter(t,
|
||||
berRawEquality("uid", "jsmith"),
|
||||
berRawEquality("uid", "jdoe"),
|
||||
),
|
||||
want: `ID:"jsmith" ID:"jdoe"`,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := s.buildBleveQuery(tc.filter)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Assertion values are parsed by Bleve, so they must not be able to
|
||||
// introduce extra query terms or operators.
|
||||
func TestBuildBleveQueryEscapesValues(t *testing.T) {
|
||||
s := &server{l: testLogger()}
|
||||
|
||||
cases := []struct {
|
||||
value string
|
||||
want string
|
||||
}{
|
||||
{"x Name:y", `ID:"x Name:y"`},
|
||||
{`x" OR ID:"*`, `ID:"x\" OR ID:\"*"`},
|
||||
{`back\slash`, `ID:"back\\slash"`},
|
||||
{"x AND Name:admin", `ID:"x AND Name:admin"`},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, err := s.buildBleveQuery(berEqualityFilter(t, "uid", tc.value))
|
||||
if err != nil {
|
||||
t.Fatalf("%q: unexpected error: %s", tc.value, err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("value %q: got %q, want %q", tc.value, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBleveQueryUnsupported(t *testing.T) {
|
||||
s := &server{l: testLogger()}
|
||||
|
||||
if _, err := s.buildBleveQuery(berPresentFilter(t, "mail")); err == nil {
|
||||
t.Error("an unsupported attribute should not produce a query")
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,52 @@ func berSearchRequest(baseDN string) []byte {
|
|||
return berElement(0x30, berInteger(2), search)
|
||||
}
|
||||
|
||||
// berEqualityFilter encodes an equalityMatch [3]
|
||||
// AttributeValueAssertion filter and decodes it through the real BER
|
||||
// reader. The filter types have unexported fields and no exported
|
||||
// constructors, so this is the only way to build one for a test.
|
||||
func berEqualityFilter(t *testing.T, attr, value string) message.Filter {
|
||||
t.Helper()
|
||||
return berDecodeFilter(t, berElement(0xa3, berOctetString(attr), berOctetString(value)))
|
||||
}
|
||||
|
||||
// berPresentFilter encodes a present [7] AttributeDescription filter.
|
||||
func berPresentFilter(t *testing.T, attr string) message.Filter {
|
||||
t.Helper()
|
||||
return berDecodeFilter(t, berElement(0x87, []byte(attr)))
|
||||
}
|
||||
|
||||
// berOrFilter encodes an OR [1] of already-encoded filter elements.
|
||||
func berOrFilter(t *testing.T, filters ...[]byte) message.Filter {
|
||||
t.Helper()
|
||||
return berDecodeFilter(t, berElement(0xa1, filters...))
|
||||
}
|
||||
|
||||
// berRawEquality returns the encoded bytes of an equality match so it
|
||||
// can be composed into a nested filter.
|
||||
func berRawEquality(attr, value string) []byte {
|
||||
return berElement(0xa3, berOctetString(attr), berOctetString(value))
|
||||
}
|
||||
|
||||
// berDecodeFilter wraps a raw filter element in a minimal search
|
||||
// request, decodes it and returns the parsed filter.
|
||||
func berDecodeFilter(t *testing.T, filterElement []byte) message.Filter {
|
||||
t.Helper()
|
||||
search := berElement(0x63,
|
||||
berOctetString(""), // baseObject
|
||||
berElement(0x0a, []byte{2}), // scope wholeSubtree
|
||||
berElement(0x0a, []byte{0}), // derefAliases never
|
||||
berInteger(0), // sizeLimit
|
||||
berInteger(0), // timeLimit
|
||||
berElement(0x01, []byte{0}), // typesOnly false
|
||||
filterElement, // filter
|
||||
berElement(0x30), // empty attribute selection
|
||||
)
|
||||
m := mustDecode(t, berElement(0x30, berInteger(1), search))
|
||||
r := m.GetSearchRequest()
|
||||
return r.Filter()
|
||||
}
|
||||
|
||||
func mustDecode(t *testing.T, raw []byte) *ldap.Message {
|
||||
t.Helper()
|
||||
m, err := message.ReadLDAPMessage(message.NewBytes(0, raw))
|
||||
|
|
|
|||
Loading…
Reference in a new issue