mirror of
https://github.com/hanzoai/xorm.git
synced 2026-10-30 14:55:47 +00:00
quoteWordTo wrapped an identifier in the dialect delimiter but wrote the body
verbatim, so a delimiter inside the identifier closed the quote the writer
opened and the remainder ran as SQL. Desc/Asc route caller strings straight
here through Quoter.QuoteTo:
Desc("name`,(select token from secret)--")
-> ORDER BY `name`,(select token from secret)--` DESC
two quoted identifiers plus a bare subquery, not one identifier. The word looks
quoted, so callers treat Desc/Asc as identifier-safe; it was not.
Double the closing delimiter — the standard SQL identifier escape — and force a
quoted form for any word that carries one, even a dialect that would otherwise
emit it bare. A name that smuggled a delimiter becomes one identifier that does
not exist, so it fails closed:
-> ORDER BY `name``,(select token from secret)--` DESC (no such column)
Same for the " and ] delimiters. Legitimate names hold no delimiter and are
unchanged, so the existing quoter cases and the round-trip through Trim still
hold. Tests assert the emitted quoted string per dialect.
Co-authored-by: Hanzo Dev <[email protected]>
64 lines
2.1 KiB
Go
64 lines
2.1 KiB
Go
// Copyright 2024 The Xorm Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package schemas
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// quoteWordTo wraps an identifier in the dialect delimiter but wrote the body
|
|
// verbatim, so a delimiter INSIDE the identifier closed the quote the writer
|
|
// opened and the remainder executed. Desc("name`,(subquery)--") became
|
|
//
|
|
// `name`,(subquery)--`
|
|
//
|
|
// two quoted identifiers plus bare SQL, not one identifier. The escape is to
|
|
// DOUBLE the closing delimiter (standard SQL identifier escaping); this asserts
|
|
// the emitted string, so it fails if the escape is dropped.
|
|
func TestQuoteWordCannotBreakOutOfItsDelimiter(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
quoter Quoter
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
name: "backtick (mysql/sqlite)",
|
|
quoter: Quoter{'`', '`', AlwaysReserve},
|
|
in: "name`,(select/**/token/**/from/**/secret)--",
|
|
want: "`name``,(select/**/token/**/from/**/secret)--`",
|
|
},
|
|
{
|
|
name: "double-quote (postgres/standard)",
|
|
quoter: Quoter{'"', '"', AlwaysReserve},
|
|
in: `name",(select/**/token/**/from/**/secret)--`,
|
|
want: `"name"",(select/**/token/**/from/**/secret)--"`,
|
|
},
|
|
{
|
|
name: "bracket (mssql) — only the closing ] is doubled",
|
|
quoter: Quoter{'[', ']', AlwaysReserve},
|
|
in: `name],(select/**/token/**/from/**/secret)--`,
|
|
want: `[name]],(select/**/token/**/from/**/secret)--]`,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got := c.quoter.Quote(c.in)
|
|
t.Logf("Quote(%q) = %q", c.in, got)
|
|
assert.Equal(t, c.want, got)
|
|
|
|
// Structural invariant: strip the outer delimiter pair; every closing
|
|
// delimiter left in the body must be doubled, so its count is even.
|
|
// A lone one would terminate the quote and start bare SQL.
|
|
inner := got[1 : len(got)-1]
|
|
assert.Equal(t, 0, strings.Count(inner, string(c.quoter.Suffix))%2,
|
|
"interior closing delimiters must all be escaped (doubled)")
|
|
})
|
|
}
|
|
}
|