Skip to content

Commit fb26dc8

Browse files
authored
fix: libddwaf does not accept nullptr-valued blank strings (#120)
Inadvertently replaced the pointer to a `\x00` byte that was used when encoding a blank string with a `0` (`null`) pointer; which `libddwaf` does not currently accept. This commit restores the previous encoding behavior, and adds a test that was created based on the ruleset that allowed discovery of this issue. --- Also fixes up the CGO-exported symbol name to be at V4 so it does not clash with the one exported by V3.
1 parent e75c425 commit fb26dc8

File tree

8 files changed

+154
-19
lines changed

8 files changed

+154
-19
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,12 @@ Another requirement of `libddwaf` is to have a FHS filesystem on your machine an
143143
- Do not use `uintptr` as function arguments or results types, coming from `unsafe.Pointer` casts of Go values, because they escape the pointer analysis which can create wrongly optimized code and crash. Pointer arithmetic is of course necessary in such a library but must be kept in the same function scope.
144144
- GDB is available on arm64 but is not officially supported so it usually crashes pretty fast (as of June 2023)
145145
- No pointer to variables on the stack shall be sent to the C code because Go stacks can be moved during the C call. More on this [here](https://medium.com/@trinad536/escape-analysis-in-golang-fc81b78f3550)
146+
147+
## Debugging
148+
149+
Debug-logging can be enabled for underlying C/C++ library by building (or testing) by setting the
150+
`DD_APPSEC_WAF_LOG_LEVEL` environment variable to one of: `trace`, `debug`, `info`, `warn` (or
151+
`warning`), `error`, `off` (which is the default behavior and logs nothing).
152+
153+
The `DD_APPSEC_WAF_LOG_FILTER` environment variable can be set to a valid (per the `regexp` package)
154+
regular expression to limit logging to only messages that match the regular expression.

builder_test.go

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@
88
package libddwaf
99

1010
import (
11+
"bytes"
1112
"encoding/json"
1213
"maps"
1314
"net/http"
1415
"os"
1516
"testing"
17+
"time"
1618

17-
"github.com/DataDog/go-libddwaf/v4/internal/log"
1819
"github.com/DataDog/go-libddwaf/v4/timer"
20+
"github.com/stretchr/testify/assert"
1921
"github.com/stretchr/testify/require"
2022
)
2123

2224
func TestBuilder(t *testing.T) {
23-
wafLib.SetLogCb(log.CallbackFunctionPointer(), log.LevelDebug)
24-
2525
if supported, err := Usable(); !supported || err != nil {
2626
t.Skipf("target is not supported by the WAF: %v", err)
2727
}
@@ -299,4 +299,89 @@ func TestBuilder(t *testing.T) {
299299
require.NotNil(t, handle)
300300
handle.Close()
301301
})
302+
303+
t.Run("blank-string-encoding", func(t *testing.T) {
304+
var rulesJSON = `{
305+
"version": "2.1",
306+
"metadata": {
307+
"rules_version": "1.2.6"
308+
},
309+
"rules": [
310+
{
311+
"id": "canary_rule4",
312+
"name": "Canary 4",
313+
"tags": {
314+
"type": "security_scanner",
315+
"category": "attack_attempt"
316+
},
317+
"conditions": [
318+
{
319+
"parameters": {
320+
"inputs": [
321+
{
322+
"address": "server.request.headers.no_cookies",
323+
"key_path": [
324+
"user-agent"
325+
]
326+
}
327+
],
328+
"regex": "^Canary\\/v4"
329+
},
330+
"operator": "match_regex"
331+
}
332+
],
333+
"on_match": [
334+
"block4"
335+
]
336+
}
337+
],
338+
"actions": [
339+
{
340+
"id": "block4",
341+
"type": "redirect_request",
342+
"parameters": {
343+
"status_code": 303,
344+
"location": ""
345+
}
346+
}
347+
]
348+
}
349+
`
350+
351+
builder, err := NewBuilder("", "")
352+
require.NoError(t, err)
353+
354+
dec := json.NewDecoder(bytes.NewReader([]byte(rulesJSON)))
355+
dec.UseNumber()
356+
357+
var rules map[string]any
358+
require.NoError(t, dec.Decode(&rules))
359+
360+
diag, err := builder.AddOrUpdateConfig("/", rules)
361+
require.NoError(t, err)
362+
diag.EachFeature(func(name string, feat *Feature) {
363+
assert.Empty(t, feat.Error, "feature %s has top-level error", name)
364+
assert.Empty(t, feat.Errors, "feature %s has errors", name)
365+
assert.Empty(t, feat.Warnings, "feature %s has warnings", name)
366+
})
367+
368+
waf := builder.Build()
369+
require.NotNil(t, waf)
370+
defer waf.Close()
371+
372+
ctx, err := waf.NewContext(time.Hour)
373+
require.NoError(t, err)
374+
defer ctx.Close()
375+
376+
res, err := ctx.Run(RunAddressData{
377+
Persistent: map[string]any{
378+
"server.request.headers.no_cookies": map[string][]string{
379+
"user-agent": {"Canary/v4 bazinga"},
380+
},
381+
},
382+
})
383+
require.NoError(t, err)
384+
assert.NotEmpty(t, res.Events)
385+
assert.NotEmpty(t, res.Actions)
386+
})
302387
}

encoder.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package libddwaf
77

88
import (
99
"context"
10+
"encoding/json"
1011
"fmt"
1112
"math"
1213
"reflect"
@@ -143,6 +144,8 @@ var nullableTypeKinds = map[reflect.Kind]struct{}{
143144
reflect.Chan: {},
144145
}
145146

147+
var jsonNumberType = reflect.TypeOf(json.Number(""))
148+
146149
// isValueNil check if the value is nullable and if it is actually nil
147150
// we cannot directly use value.IsNil() because it panics on non-pointer values
148151
func isValueNil(value reflect.Value) bool {
@@ -189,6 +192,10 @@ func (encoder *encoder) encode(value reflect.Value, obj *bindings.WAFObject, dep
189192
case value.CanFloat(): // any float type or alias
190193
encodeNative(unsafe.NativeToUintptr(value.Float()), bindings.WAFFloatType, obj)
191194

195+
// json.Number -- string-represented arbitrary precision numbers
196+
case value.Type() == jsonNumberType:
197+
encoder.encodeJSONNumber(value.Interface().(json.Number), obj)
198+
192199
// Strings
193200
case kind == reflect.String: // string type
194201
encoder.encodeString(value.String(), obj)
@@ -221,6 +228,23 @@ func (encoder *encoder) encode(value reflect.Value, obj *bindings.WAFObject, dep
221228
return nil
222229
}
223230

231+
func (encoder *encoder) encodeJSONNumber(num json.Number, obj *bindings.WAFObject) {
232+
// Important to attempt int64 first, as this is lossless. Values that are either too small or too
233+
// large to be represented as int64 can be represented as float64, but this can be lossy.
234+
if i, err := num.Int64(); err == nil {
235+
encodeNative(uintptr(i), bindings.WAFIntType, obj)
236+
return
237+
}
238+
239+
if f, err := num.Float64(); err == nil {
240+
encodeNative(unsafe.NativeToUintptr(f), bindings.WAFFloatType, obj)
241+
return
242+
}
243+
244+
// Could not store as int64 nor float, so we'll store it as a string...
245+
encoder.encodeString(num.String(), obj)
246+
}
247+
224248
func (encoder *encoder) encodeString(str string, obj *bindings.WAFObject) {
225249
size := len(str)
226250
if size > encoder.stringMaxSize {

encoder_decoder_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ package libddwaf
1010
import (
1111
"context"
1212
"encoding/json"
13+
"math"
1314
"reflect"
1415
"runtime"
1516
"sort"
17+
"strconv"
1618
"testing"
1719
"time"
1820

@@ -248,6 +250,26 @@ func TestEncodeDecode(t *testing.T) {
248250
Input: map[any]any{"k1": uint64(1), new(string): "string pointer key", "k2": "2"},
249251
Output: map[string]any{"k1": uint64(1), "": "string pointer key", "k2": "2"},
250252
},
253+
{
254+
Name: "json-number.int",
255+
Input: json.Number("1234567890"),
256+
Output: int64(1234567890),
257+
},
258+
{
259+
Name: "json-number.bigint",
260+
Input: json.Number(strconv.FormatUint(math.MaxUint64, 10) + "999"),
261+
Output: math.MaxUint64*1000.0 + 999, // Gets represented as a float64
262+
},
263+
{
264+
Name: "json-number.float",
265+
Input: json.Number("1234567890.42"),
266+
Output: 1234567890.42,
267+
},
268+
{
269+
Name: "json-number.bogus",
270+
Input: json.Number("bogus"),
271+
Output: "bogus",
272+
},
251273
{
252274
Name: "struct",
253275
Input: struct {
@@ -346,7 +368,7 @@ func TestEncodeDecode(t *testing.T) {
346368
}
347369

348370
require.NoError(t, err, "unexpected error when decoding: %v", err)
349-
require.True(t, reflect.DeepEqual(tc.Output, val), "expected %v, got %v", tc.Output, val)
371+
require.True(t, reflect.DeepEqual(tc.Output, val), "expected %#v, got %#v", tc.Output, val)
350372
})
351373

352374
t.Run("run", func(t *testing.T) {

internal/bindings/ctypes.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,16 @@ func (w *WAFObject) SetMapKey(pinner pin.Pinner, key string) {
137137
w.ParameterName = uintptr(unsafe.Pointer(header.Data))
138138
}
139139

140+
var blankCStringValue = unsafe.Pointer(unsafe.NativeStringUnwrap("\x00").Data)
141+
140142
// SetString sets the receiving [WAFObject] value to the given string.
141143
func (w *WAFObject) SetString(pinner pin.Pinner, str string) {
142144
header := unsafe.NativeStringUnwrap(str)
143145

144146
w.Type = WAFStringType
145147
w.NbEntries = uint64(header.Len)
146148
if w.NbEntries == 0 {
147-
w.Value = 0
149+
w.Value = uintptr(blankCStringValue)
148150
return
149151
}
150152
pinner.Pin(unsafe.Pointer(header.Data))

internal/bindings/waf_dl.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,10 @@ func NewWAFLib() (dl *WAFLib, err error) {
6565
}
6666

6767
if val := os.Getenv(log.EnvVarLogLevel); val != "" {
68-
setLogSym, symErr := purego.Dlsym(handle, "ddwaf_set_log_cb")
69-
if symErr != nil {
70-
return nil, fmt.Errorf("get symbol: %w", symErr)
71-
}
7268
logLevel := log.LevelNamed(val)
73-
dl.syscall(setLogSym, log.CallbackFunctionPointer(), uintptr(logLevel))
69+
if logLevel != log.LevelOff {
70+
dl.SetLogCb(log.CallbackFunctionPointer(), logLevel)
71+
}
7472
}
7573

7674
return

internal/log/log_cgo.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
package log
99

1010
// #include "./ddwaf.h"
11-
// extern void ddwafLogCallbackFnv3(
11+
// extern void ddwafLogCallbackFnV4(
1212
// DDWAF_LOG_LEVEL level,
1313
// char* function,
1414
// char* file,
@@ -22,11 +22,11 @@ import "github.com/DataDog/go-libddwaf/v4/internal/unsafe"
2222
// CallbackFunctionPointer returns a pointer to the log callback function which
2323
// can be used with libddwaf.
2424
func CallbackFunctionPointer() uintptr {
25-
return uintptr(C.ddwafLogCallbackFnv3)
25+
return uintptr(C.ddwafLogCallbackFnV4)
2626
}
2727

28-
//export ddwafLogCallbackFnv3
29-
func ddwafLogCallbackFnv3(level C.DDWAF_LOG_LEVEL, fnPtr, filePtr *C.char, line C.unsigned, msgPtr *C.char, _ C.uint64_t) {
28+
//export ddwafLogCallbackFnV4
29+
func ddwafLogCallbackFnV4(level C.DDWAF_LOG_LEVEL, fnPtr, filePtr *C.char, line C.unsigned, msgPtr *C.char, _ C.uint64_t) {
3030
function := unsafe.Gostring(unsafe.CastNative[C.char, byte](fnPtr))
3131
file := unsafe.Gostring(unsafe.CastNative[C.char, byte](filePtr))
3232
message := unsafe.Gostring(unsafe.CastNative[C.char, byte](msgPtr))

waf_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424

2525
"github.com/DataDog/go-libddwaf/v4/internal/bindings"
2626
"github.com/DataDog/go-libddwaf/v4/internal/lib"
27-
"github.com/DataDog/go-libddwaf/v4/internal/log"
2827
"github.com/DataDog/go-libddwaf/v4/timer"
2928
"github.com/DataDog/go-libddwaf/v4/waferrors"
3029
"github.com/stretchr/testify/require"
@@ -859,10 +858,6 @@ func TestConcurrency(t *testing.T) {
859858
// This test validates that the WAF Context can be used from multiple
860859
// threads, with mixed calls to `ddwaf_run` and `ddwaf_context_destroy`,
861860
// which are not thread-safe.
862-
863-
wafLib.SetLogCb(log.CallbackFunctionPointer(), log.LevelDebug)
864-
t.Cleanup(func() { wafLib.SetLogCb(0, log.LevelOff) })
865-
866861
waf, _, err := newDefaultHandle(testArachniRule)
867862
require.NoError(t, err)
868863

0 commit comments

Comments
 (0)