├── lib ├── vendor │ ├── libinjection │ │ └── .keep │ ├── Makefile │ ├── Dockerfile.linux_amd64 │ ├── Dockerfile.linux_aarch64 │ └── COPYING ├── libinjection │ ├── linux_amd64 │ │ └── libinjection.so │ ├── linux_aarch64 │ │ └── libinjection.so │ └── darwin_aarch64 │ │ └── libinjection.so ├── libinjection_linux_amd64.go ├── libinjection_linux_aarch64.go ├── libinjection_darwin_aarch64.go ├── limitter │ ├── limitter.go │ └── limitter_test.go ├── libinjection.go └── libinjection_test.go ├── waf ├── wafcontext │ ├── meta.go │ └── operation.go ├── event_recorder.go ├── waf.go ├── event.go ├── evaluator.go └── waf_test.go ├── renovate.json ├── internal ├── listener │ ├── listener.go │ ├── os │ │ └── file.go │ ├── sql │ │ └── sql.go │ ├── http │ │ ├── client.go │ │ └── http.go │ ├── account_takeover │ │ └── listener.go │ └── graphql │ │ └── graphql.go ├── inspector │ ├── libinjection │ │ ├── xss.go │ │ ├── sqli.go │ │ ├── sqli_test.go │ │ └── xss_test.go │ ├── ssrf │ │ ├── metadata_service_test.go │ │ └── matadata_service.go │ ├── ssrf.go │ ├── lfi │ │ └── lfi.go │ ├── sqli │ │ ├── comment.go │ │ ├── comment_test.go │ │ ├── tautology_test.go │ │ └── tautology.go │ ├── lfi.go │ ├── sqli.go │ ├── regex.go │ ├── match_list.go │ ├── account_takeover.go │ ├── account_takeover │ │ └── login.go │ ├── types │ │ └── values.go │ ├── inspector.go │ ├── libinjection.go │ ├── match_list_test.go │ ├── target.go │ └── account_takeover_test.go ├── rule │ ├── testdata │ │ ├── loader.go │ │ └── rules.json │ ├── validator │ │ └── validation.go │ └── rule.go ├── emitter │ ├── http │ │ ├── parser │ │ │ ├── parser.go │ │ │ ├── form.go │ │ │ ├── json.go │ │ │ ├── json_test.go │ │ │ └── form_test.go │ │ ├── handler.go │ │ ├── client_handler_test.go │ │ ├── client_handler.go │ │ ├── http.go │ │ ├── http_test.go │ │ └── handler_test.go │ ├── sql │ │ ├── handler.go │ │ └── handler_test.go │ ├── graphql │ │ ├── handler_test.go │ │ └── handler.go │ ├── os │ │ ├── handler.go │ │ └── handler_test.go │ ├── account_takeover │ │ └── handler.go │ └── waf │ │ └── operation.go └── log │ ├── logger.go │ └── logger_test.go ├── contrib ├── 99designs │ └── gqlgen │ │ ├── testserver │ │ └── graph │ │ │ ├── resolver.go │ │ │ ├── schema.graphqls │ │ │ ├── model │ │ │ └── models_gen.go │ │ │ └── schema.resolvers.go │ │ ├── README.md │ │ └── waf.go ├── database │ └── sql │ │ ├── tx.go │ │ ├── driver_test.go │ │ ├── stmt.go │ │ ├── README.md │ │ ├── driver.go │ │ ├── conn.go │ │ └── conn_test.go ├── net │ └── http │ │ ├── client.go │ │ ├── waf.go │ │ ├── client_test.go │ │ ├── README.md │ │ └── waf_test.go ├── application │ └── account_takeover.go ├── gin-gonic │ └── gin │ │ ├── waf.go │ │ ├── README.md │ │ └── waf_test.go ├── labstack │ └── echo │ │ ├── waf.go │ │ ├── README.md │ │ └── waf_test.go ├── os │ ├── waf.go │ └── README.md └── gorm.io │ └── gorm │ ├── example_test.go │ └── README.md ├── exporter ├── nop.go ├── chan.go ├── stdout.go └── exporter.go ├── .github └── workflows │ └── test.yml ├── examples └── auth │ ├── go.mod │ ├── README.md │ ├── main.go │ └── go.sum ├── handler ├── error.go ├── error_test.go └── response │ ├── templates │ └── blocked.html │ ├── handler.go │ ├── response_writer.go │ ├── response_writer_test.go │ └── handler_test.go ├── go.mod └── waffle.go /lib/vendor/libinjection/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/libinjection/linux_amd64/libinjection.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitebatch/waffle-go/HEAD/lib/libinjection/linux_amd64/libinjection.so -------------------------------------------------------------------------------- /lib/libinjection/linux_aarch64/libinjection.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitebatch/waffle-go/HEAD/lib/libinjection/linux_aarch64/libinjection.so -------------------------------------------------------------------------------- /waf/wafcontext/meta.go: -------------------------------------------------------------------------------- 1 | package wafcontext 2 | 3 | type ReservedMetaKey string 4 | 5 | const ( 6 | UserID ReservedMetaKey = "UserID" 7 | ) 8 | -------------------------------------------------------------------------------- /lib/libinjection/darwin_aarch64/libinjection.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitebatch/waffle-go/HEAD/lib/libinjection/darwin_aarch64/libinjection.so -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>sitebatch/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/libinjection_linux_amd64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && amd64 2 | // +build linux,amd64 3 | 4 | package lib 5 | 6 | import _ "embed" 7 | 8 | //go:embed libinjection/linux_amd64/libinjection.so 9 | var LibinjectionSharedLib []byte 10 | -------------------------------------------------------------------------------- /lib/libinjection_linux_aarch64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && arm64 2 | // +build linux,arm64 3 | 4 | package lib 5 | 6 | import _ "embed" 7 | 8 | //go:embed libinjection/linux_aarch64/libinjection.so 9 | var LibinjectionSharedLib []byte 10 | -------------------------------------------------------------------------------- /lib/libinjection_darwin_aarch64.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && arm64 2 | // +build darwin,arm64 3 | 4 | package lib 5 | 6 | import _ "embed" 7 | 8 | //go:embed libinjection/darwin_aarch64/libinjection.so 9 | var LibinjectionSharedLib []byte 10 | -------------------------------------------------------------------------------- /internal/listener/listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import "github.com/sitebatch/waffle-go/internal/operation" 4 | 5 | type Listener interface { 6 | Name() string 7 | } 8 | 9 | type NewListener func(operation.Operation) (Listener, error) 10 | -------------------------------------------------------------------------------- /contrib/99designs/gqlgen/testserver/graph/resolver.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | // This file will not be regenerated automatically. 4 | // 5 | // It serves as dependency injection for your app, add any dependencies you require here. 6 | 7 | type Resolver struct{} 8 | -------------------------------------------------------------------------------- /contrib/database/sql/tx.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import "database/sql/driver" 4 | 5 | type waffleTx struct { 6 | driver.Tx 7 | } 8 | 9 | func (t waffleTx) Commit() error { 10 | return t.Tx.Commit() 11 | } 12 | 13 | func (t waffleTx) Rollback() error { 14 | return t.Tx.Rollback() 15 | } 16 | -------------------------------------------------------------------------------- /contrib/net/http/client.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | httpEmitter "github.com/sitebatch/waffle-go/internal/emitter/http" 7 | ) 8 | 9 | func WrapClient(c *http.Client, opts ...httpEmitter.RoundTripOption) *http.Client { 10 | return httpEmitter.WrapClient(c, opts...) 11 | } 12 | -------------------------------------------------------------------------------- /internal/inspector/libinjection/xss.go: -------------------------------------------------------------------------------- 1 | package libinjection 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sitebatch/waffle-go/lib" 7 | ) 8 | 9 | func IsXSSPayload(value string) error { 10 | if isXSS := lib.LibinjectionXSSFunc(value, len(value)); isXSS == 1 { 11 | return fmt.Errorf("XSS detected") 12 | } 13 | 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/rule/testdata/loader.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func MustReadRule(t *testing.T, filename string) []byte { 9 | t.Helper() 10 | 11 | rule, err := os.ReadFile(filename) 12 | if err != nil { 13 | t.Fatalf("failed to read rule file: %v", err) 14 | } 15 | 16 | return rule 17 | } 18 | -------------------------------------------------------------------------------- /internal/inspector/libinjection/sqli.go: -------------------------------------------------------------------------------- 1 | package libinjection 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sitebatch/waffle-go/lib" 7 | ) 8 | 9 | func IsSQLiPayload(value string) error { 10 | var fingerprint string 11 | isSQLi := lib.LibinjectionSQLiFunc(value, len(value), fingerprint) 12 | if isSQLi == 1 { 13 | return fmt.Errorf("SQLi detected") 14 | } 15 | 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /exporter/nop.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sitebatch/waffle-go/waf" 7 | ) 8 | 9 | var _ EventExporter = (*nopExporter)(nil) 10 | 11 | type nopExporter struct{} 12 | 13 | func newNopExporter() *nopExporter { 14 | return &nopExporter{} 15 | } 16 | 17 | func (e *nopExporter) Export(_ context.Context, _ waf.ReadOnlyDetectionEvents) error { 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /contrib/net/http/waf.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | httpHandler "github.com/sitebatch/waffle-go/internal/emitter/http" 7 | ) 8 | 9 | // WafMiddleware is a middleware for lanstack/echo that protects common web attacks. 10 | func WafMiddleware(next http.Handler) http.Handler { 11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | httpHandler.WrapHandler(next, httpHandler.Options{}).ServeHTTP(w, r) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /exporter/chan.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sitebatch/waffle-go/waf" 7 | ) 8 | 9 | var _ EventExporter = (*ChanExporter)(nil) 10 | 11 | type ChanExporter struct { 12 | wCh chan waf.ReadOnlyDetectionEvents 13 | } 14 | 15 | func NewChanExporter(ch chan waf.ReadOnlyDetectionEvents) *ChanExporter { 16 | return &ChanExporter{ 17 | wCh: ch, 18 | } 19 | } 20 | 21 | func (e *ChanExporter) Export(_ context.Context, event waf.ReadOnlyDetectionEvents) error { 22 | e.wCh <- event 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /contrib/99designs/gqlgen/testserver/graph/schema.graphqls: -------------------------------------------------------------------------------- 1 | # GraphQL schema example 2 | # 3 | # https://gqlgen.com/getting-started/ 4 | 5 | type Todo { 6 | id: ID! 7 | text: String! 8 | done: Boolean! 9 | user: User! 10 | } 11 | 12 | type User { 13 | id: ID! 14 | name: String! 15 | } 16 | 17 | type Query { 18 | todos: [Todo!]! 19 | searchTodo(id: ID!, text: String!): [Todo!] 20 | } 21 | 22 | input NewTodo { 23 | text: String! 24 | userId: String! 25 | } 26 | 27 | type Mutation { 28 | createTodo(input: NewTodo!): Todo! 29 | } 30 | -------------------------------------------------------------------------------- /contrib/99designs/gqlgen/testserver/graph/model/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | type Mutation struct { 6 | } 7 | 8 | type NewTodo struct { 9 | Text string `json:"text"` 10 | UserID string `json:"userId"` 11 | } 12 | 13 | type Query struct { 14 | } 15 | 16 | type Todo struct { 17 | ID string `json:"id"` 18 | Text string `json:"text"` 19 | Done bool `json:"done"` 20 | User *User `json:"user"` 21 | } 22 | 23 | type User struct { 24 | ID string `json:"id"` 25 | Name string `json:"name"` 26 | } 27 | -------------------------------------------------------------------------------- /lib/vendor/Makefile: -------------------------------------------------------------------------------- 1 | build/linux/amd64: 2 | $(shell docker build -t libinjection-build-linux-amd64 -f Dockerfile.linux_amd64 .) 3 | $(shell docker run --rm -v $(PWD)/libinjection:/work libinjection-build-linux-amd64 cp libinjection.amd64.so /work/linux_amd64/libinjection.so) 4 | 5 | build/linux/aarch64: 6 | $(shell docker build -t libinjection-build-linux-aarch64 -f Dockerfile.linux_aarch64 .) 7 | $(shell docker run --rm -v $(PWD)/libinjection:/work libinjection-build-linux-aarch64 cp libinjection.aarch64.so /work/linux_aarch64/libinjection.so) 8 | 9 | copy: 10 | $(shell cp -a libinjection ../) -------------------------------------------------------------------------------- /internal/rule/validator/validation.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sitebatch/waffle-go/internal/inspector" 7 | ) 8 | 9 | func ValidateInspector(inspectorName, target string) error { 10 | inspectors := inspector.NewInspectors() 11 | i, ok := inspectors[inspector.InspectorName(inspectorName)] 12 | if !ok { 13 | return fmt.Errorf("inspector %s not found", inspectorName) 14 | } 15 | 16 | if !i.IsSupportTarget(inspector.InspectTarget(target)) { 17 | return fmt.Errorf("inspector %s does not support target %s", inspectorName, target) 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | platform: [ubuntu-latest, macos-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 20 | with: 21 | go-version-file: go.mod 22 | 23 | - name: Test 24 | run: go test -v ./... 25 | -------------------------------------------------------------------------------- /contrib/application/account_takeover.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sitebatch/waffle-go/internal/emitter/account_takeover" 7 | ) 8 | 9 | // ProtectAccountTakeover protects account takeover from attacks such as brute force attack, credential stuffing 10 | // NOTE: Login attempt statistics are stored in memory, so be aware that rate limiting may not work accurately if you are using a distributed system 11 | func ProtectAccountTakeover(ctx context.Context, clientIP string, userID string) error { 12 | return account_takeover.IsSuspiciousLoginActivity(ctx, clientIP, userID) 13 | } 14 | -------------------------------------------------------------------------------- /lib/limitter/limitter.go: -------------------------------------------------------------------------------- 1 | // Package limtiter provides a simple rate limit using x/time/limit. 2 | // It is mainly intended for use in fraudulent login detection, such as credential stuffing. 3 | package limitter 4 | 5 | import ( 6 | "golang.org/x/time/rate" 7 | ) 8 | 9 | type Limitter interface { 10 | Allow() bool 11 | } 12 | 13 | type SimpleLimitter struct { 14 | limiter *rate.Limiter 15 | } 16 | 17 | func NewLimitter(r rate.Limit, burst int) Limitter { 18 | return &SimpleLimitter{ 19 | limiter: rate.NewLimiter(r, burst), 20 | } 21 | } 22 | 23 | func (l *SimpleLimitter) Allow() bool { 24 | return l.limiter.Allow() 25 | } 26 | -------------------------------------------------------------------------------- /lib/vendor/Dockerfile.linux_amd64: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 ubuntu:24.04 as builder 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | git \ 6 | gcc \ 7 | clang \ 8 | make \ 9 | autoconf \ 10 | libtool 11 | 12 | RUN git clone https://github.com/libinjection/libinjection 13 | 14 | WORKDIR /libinjection 15 | 16 | RUN CFLAGS="-DLIBINJECTION_VERSION=$(date +%Y%m%d%H%M%S)" ./autogen.sh && \ 17 | ./configure && \ 18 | make 19 | 20 | WORKDIR /build/libinjection 21 | RUN cp /libinjection/src/libinjection_*.o . 22 | RUN gcc -dynamiclib -shared -o libinjection.amd64.so libinjection_sqli.o libinjection_xss.o libinjection_html5.o 23 | -------------------------------------------------------------------------------- /lib/vendor/Dockerfile.linux_aarch64: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/aarch64 ubuntu:24.04 as builder 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | git \ 6 | gcc \ 7 | clang \ 8 | make \ 9 | autoconf \ 10 | libtool 11 | 12 | RUN git clone https://github.com/libinjection/libinjection 13 | 14 | WORKDIR /libinjection 15 | 16 | RUN CFLAGS="-DLIBINJECTION_VERSION=$(date +%Y%m%d%H%M%S)" ./autogen.sh && \ 17 | ./configure && \ 18 | make 19 | 20 | WORKDIR /build/libinjection 21 | RUN cp /libinjection/src/libinjection_*.o . 22 | RUN gcc -dynamiclib -shared -o libinjection.aarch64.so libinjection_sqli.o libinjection_xss.o libinjection_html5.o 23 | -------------------------------------------------------------------------------- /internal/emitter/http/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type parser interface { 8 | Parse(r *http.Request) (map[string][]string, error) 9 | } 10 | 11 | func ParseHTTPRequestBody(r *http.Request) (map[string][]string, error) { 12 | p := newParser(r.Method, r.Header.Get("Content-Type")) 13 | return p.Parse(r) 14 | } 15 | 16 | func newParser(method, contentType string) parser { 17 | if method == http.MethodGet { 18 | return &formParser{} 19 | } 20 | 21 | switch contentType { 22 | case "application/json": 23 | return &jsonParser{} 24 | case "multipart/form-data": 25 | return &multipartParser{} 26 | default: 27 | return &formParser{} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contrib/database/sql/driver_test.go: -------------------------------------------------------------------------------- 1 | package sql_test 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | _ "github.com/mattn/go-sqlite3" 8 | waffleSql "github.com/sitebatch/waffle-go/contrib/database/sql" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRegister(t *testing.T) { 13 | t.Parallel() 14 | 15 | driverName, err := waffleSql.Register("sqlite3") 16 | assert.NoError(t, err) 17 | 18 | _, err = sql.Open(driverName, "file:test.db?cache=shared&mode=memory") 19 | assert.NoError(t, err) 20 | } 21 | 22 | func TestOpen(t *testing.T) { 23 | t.Parallel() 24 | 25 | db, err := waffleSql.Open("sqlite3", "file:test.db?cache=shared&mode=memory") 26 | assert.NoError(t, err) 27 | assert.NoError(t, db.Ping()) 28 | } 29 | -------------------------------------------------------------------------------- /waf/event_recorder.go: -------------------------------------------------------------------------------- 1 | package waf 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type EventRecorder struct { 8 | events ReadOnlyDetectionEvents 9 | mu sync.Mutex 10 | } 11 | 12 | func NewEventRecorder() *EventRecorder { 13 | return &EventRecorder{ 14 | events: nil, 15 | mu: sync.Mutex{}, 16 | } 17 | } 18 | 19 | func (er *EventRecorder) Store(events ReadOnlyDetectionEvents) { 20 | er.mu.Lock() 21 | defer er.mu.Unlock() 22 | 23 | er.events = events 24 | } 25 | 26 | func (er *EventRecorder) Load() ReadOnlyDetectionEvents { 27 | er.mu.Lock() 28 | defer er.mu.Unlock() 29 | 30 | return er.events 31 | } 32 | 33 | func (er *EventRecorder) Clear() { 34 | er.mu.Lock() 35 | defer er.mu.Unlock() 36 | 37 | er.events = nil 38 | } 39 | -------------------------------------------------------------------------------- /contrib/gin-gonic/gin/waf.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | httpHandler "github.com/sitebatch/waffle-go/internal/emitter/http" 8 | ) 9 | 10 | func wafHandler(c *gin.Context) { 11 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | c.Request = r 13 | c.Next() 14 | }) 15 | 16 | options := httpHandler.Options{ 17 | OnBlockFunc: func() { 18 | c.Abort() 19 | }, 20 | } 21 | 22 | httpHandler.WrapHandler(handler, options).ServeHTTP(c.Writer, c.Request) 23 | } 24 | 25 | // WafMiddleware is a middleware that protects HTTP requests from attacks. 26 | func WafMiddleware() gin.HandlerFunc { 27 | return func(c *gin.Context) { 28 | wafHandler(c) 29 | c.Next() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/auth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sitebatch/waffle-go/example/sql 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/mattn/go-sqlite3 v1.14.24 7 | github.com/sitebatch/waffle-go v0.0.2 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.9.0 // indirect 12 | github.com/go-logr/logr v1.4.3 // indirect 13 | github.com/go-logr/stdr v1.2.2 // indirect 14 | github.com/jeremywohl/flatten v1.0.1 // indirect 15 | github.com/tetratelabs/wazero v1.9.0 // indirect 16 | github.com/wasilibs/go-re2 v1.9.0 // indirect 17 | github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect 18 | github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // indirect 19 | golang.org/x/sys v0.31.0 // indirect 20 | golang.org/x/time v0.11.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /contrib/labstack/echo/waf.go: -------------------------------------------------------------------------------- 1 | package echo 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | httpHandler "github.com/sitebatch/waffle-go/internal/emitter/http" 8 | ) 9 | 10 | // WafMiddleware is a middleware for lanstack/echo that protects common web attacks. 11 | func WafMiddleware() echo.MiddlewareFunc { 12 | return func(next echo.HandlerFunc) echo.HandlerFunc { 13 | return func(c echo.Context) error { 14 | var err error 15 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | c.SetRequest(r) 17 | err = next(c) 18 | }) 19 | 20 | httpHandler := httpHandler.WrapHandler(handler, httpHandler.Options{}) 21 | httpHandler.ServeHTTP(c.Response().Writer, c.Request()) 22 | 23 | return err 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/emitter/http/parser/form.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | const defaultMemory = 32 << 20 9 | 10 | type formParser struct{} 11 | type multipartParser struct{} 12 | 13 | func (formParser) Parse(req *http.Request) (map[string][]string, error) { 14 | if err := req.ParseForm(); err != nil { 15 | return nil, err 16 | } 17 | 18 | if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) { 19 | return nil, err 20 | } 21 | 22 | return req.PostForm, nil 23 | } 24 | 25 | func (multipartParser) Parse(req *http.Request) (map[string][]string, error) { 26 | if err := req.ParseMultipartForm(defaultMemory); err != nil { 27 | return nil, err 28 | } 29 | 30 | return req.MultipartForm.Value, nil 31 | } 32 | -------------------------------------------------------------------------------- /exporter/stdout.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/sitebatch/waffle-go/internal/log" 8 | "github.com/sitebatch/waffle-go/waf" 9 | ) 10 | 11 | var _ EventExporter = (*StdoutExporter)(nil) 12 | 13 | type StdoutExporter struct{} 14 | 15 | func NewStdoutExporter() *StdoutExporter { 16 | return &StdoutExporter{} 17 | } 18 | 19 | func (e *StdoutExporter) Export(ctx context.Context, event waf.ReadOnlyDetectionEvents) error { 20 | for _, evt := range event.Events() { 21 | err := errors.New(evt.Message) 22 | log.Error(err, "", 23 | "detected_at", evt.DetectedAt, 24 | "request_url", evt.Context.HttpRequest.URL, 25 | "rule_id", evt.Rule.ID, 26 | "block", evt.Rule.IsBlockAction(), 27 | "meta", evt.Context.Meta, 28 | ) 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/inspector/ssrf/metadata_service_test.go: -------------------------------------------------------------------------------- 1 | package ssrf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsCloudMetadataServiceURL(t *testing.T) { 10 | t.Parallel() 11 | 12 | testCases := []struct { 13 | url string 14 | wantErr bool 15 | }{ 16 | { 17 | url: "http://metadata.google.internal", 18 | wantErr: true, 19 | }, 20 | { 21 | url: "https://example.com", 22 | wantErr: false, 23 | }, 24 | { 25 | url: "http://169.254.169.254", 26 | wantErr: true, 27 | }, 28 | } 29 | 30 | for _, tt := range testCases { 31 | tt := tt 32 | 33 | t.Run(tt.url, func(t *testing.T) { 34 | t.Parallel() 35 | 36 | err := IsCloudMetadataServiceURL(tt.url) 37 | assert.Equal(t, tt.wantErr, err != nil) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contrib/gin-gonic/gin/README.md: -------------------------------------------------------------------------------- 1 | # gin-gonic/gin 2 | 3 | This package provides a Waffle middleware for [gin](https://gin-gonic.com/). 4 | 5 | If you are using Gin web framework, you can apply WAF protection using the `WafMiddleware` provided by this package. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | go get github.com/sitebatch/waffle-go/contrib/gin-gonic/gin 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "github.com/gin-gonic/gin" 20 | "github.com/sitebatch/waffle-go" 21 | ginWaf "github.com/sitebatch/waffle-go/contrib/gin-gonic/gin" 22 | ) 23 | 24 | func main() { 25 | r := gin.Default() 26 | 27 | // Apply Waffle WAF middleware 28 | r.Use(ginWaf.WafMiddleware()) 29 | 30 | // Start Waffle 31 | if err := waffle.Start(); err != nil { 32 | panic(err) 33 | } 34 | 35 | r.Run(":8000") 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /contrib/os/waf.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | osHandler "github.com/sitebatch/waffle-go/internal/emitter/os" 8 | ) 9 | 10 | // ProtectReadFile protects file reading from attacks such as directory traversal and executes os.ReadFile. 11 | func ProtectReadFile(ctx context.Context, name string) ([]byte, error) { 12 | if err := osHandler.ProtectFileOperation(ctx, name); err != nil { 13 | return nil, err 14 | } 15 | 16 | return os.ReadFile(name) 17 | } 18 | 19 | // ProtectOpenFile protects file opening from attacks such as directory traversal and executes os.OpenFile. 20 | func ProtectOpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (*os.File, error) { 21 | if err := osHandler.ProtectFileOperation(ctx, name); err != nil { 22 | return nil, err 23 | } 24 | 25 | return os.OpenFile(name, flag, perm) 26 | } 27 | -------------------------------------------------------------------------------- /contrib/labstack/echo/README.md: -------------------------------------------------------------------------------- 1 | # labstack/echo 2 | 3 | This package provides a Waffle middleware for [Echo](https://echo.labstack.com/). 4 | 5 | If you are using Echo web framework, you can apply WAF protection using the `WafMiddleware` provided by this package. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | go get github.com/sitebatch/waffle-go/contrib/labstack/echo 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "net/http" 20 | "github.com/labstack/echo/v4" 21 | "github.com/labstack/echo/v4/middleware" 22 | "github.com/sitebatch/waffle-go" 23 | waffleEcho "github.com/sitebatch/waffle-go/contrib/labstack/echo" 24 | ) 25 | 26 | func main() { 27 | e := echo.New() 28 | 29 | // Apply Waffle WAF middleware 30 | e.Use(waffleEcho.WafMiddleware()) 31 | 32 | // Start Waffle 33 | if err := waffle.Start(); err != nil { 34 | e.Logger.Fatal(err) 35 | } 36 | 37 | e.Logger.Fatal(e.Start(":1323")) 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /internal/emitter/http/parser/json.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/jeremywohl/flatten" 10 | ) 11 | 12 | type jsonParser struct{} 13 | 14 | func (jsonParser) Parse(req *http.Request) (map[string][]string, error) { 15 | ctx := req.Context() 16 | copy := req.Clone(ctx) 17 | b, err := io.ReadAll(copy.Body) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer copy.Body.Close() 22 | 23 | req.Body = io.NopCloser(bytes.NewBuffer(b)) 24 | 25 | var j map[string]any 26 | 27 | if err := json.Unmarshal(b, &j); err != nil { 28 | return nil, err 29 | } 30 | 31 | flat, err := flatten.Flatten(j, "", flatten.DotStyle) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | result := make(map[string][]string) 37 | for k, v := range flat { 38 | if s, ok := v.(string); ok { 39 | result[k] = []string{s} 40 | } 41 | } 42 | 43 | return result, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/inspector/ssrf.go: -------------------------------------------------------------------------------- 1 | package inspector 2 | 3 | import ( 4 | "github.com/sitebatch/waffle-go/internal/inspector/ssrf" 5 | ) 6 | 7 | type SSRFInspector struct{} 8 | type SSRFInspectorArgs struct{} 9 | 10 | func NewSSRFInspector() Inspector { 11 | return &SSRFInspector{} 12 | } 13 | 14 | func (i *SSRFInspector) IsSupportTarget(target InspectTarget) bool { 15 | return target == InspectTargetHttpClientRequestURL 16 | } 17 | 18 | func (i *SSRFInspector) Inspect(inspectData InspectData, args InspectorArgs) (*InspectResult, error) { 19 | inspectValue := inspectData.Target[InspectTargetHttpClientRequestURL] 20 | if inspectValue == nil { 21 | return nil, nil 22 | } 23 | 24 | url := inspectValue.GetValue() 25 | 26 | if err := ssrf.IsCloudMetadataServiceURL(url); err != nil { 27 | return &InspectResult{ 28 | Target: InspectTargetHttpClientRequestURL, 29 | Payload: url, 30 | Message: err.Error(), 31 | }, nil 32 | } 33 | 34 | return nil, nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "sync/atomic" 7 | 8 | "github.com/go-logr/logr" 9 | "github.com/go-logr/stdr" 10 | ) 11 | 12 | var globalLogger = func() *atomic.Pointer[logr.Logger] { 13 | l := stdr.New(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)) 14 | 15 | p := new(atomic.Pointer[logr.Logger]) 16 | p.Store(&l) 17 | return p 18 | }() 19 | 20 | func GetLogger() logr.Logger { 21 | return *globalLogger.Load() 22 | } 23 | 24 | func SetLogger(logger logr.Logger) { 25 | globalLogger.Store(&logger) 26 | } 27 | 28 | func Debug(format string, v ...any) { 29 | GetLogger().V(8).Info(format, v...) 30 | } 31 | 32 | func Info(format string, v ...any) { 33 | GetLogger().V(4).Info(format, v...) 34 | } 35 | 36 | func Warn(format string, v ...any) { 37 | GetLogger().V(1).Info(format, v...) 38 | } 39 | 40 | func Error(err error, msg string, keysAndValues ...any) { 41 | GetLogger().Error(err, msg, keysAndValues...) 42 | } 43 | -------------------------------------------------------------------------------- /exporter/exporter.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | 7 | "github.com/sitebatch/waffle-go/waf" 8 | ) 9 | 10 | var ( 11 | globalExporter = defaultExporterValue() 12 | ) 13 | 14 | type eventExporterProvider struct { 15 | ep EventExporter 16 | } 17 | 18 | // EventExporter exports WAF detection events to any desired location. 19 | type EventExporter interface { 20 | // Export transforms and transmits event data to any desired location. 21 | Export(ctx context.Context, event waf.ReadOnlyDetectionEvents) error 22 | } 23 | 24 | func GetExporter() EventExporter { 25 | v := globalExporter.Load().(*eventExporterProvider) 26 | return v.ep 27 | } 28 | 29 | func SetExporter(exporter EventExporter) { 30 | globalExporter.Store(&eventExporterProvider{ 31 | ep: exporter, 32 | }) 33 | } 34 | 35 | func defaultExporterValue() *atomic.Value { 36 | v := &atomic.Value{} 37 | v.Store(&eventExporterProvider{ 38 | ep: newNopExporter(), 39 | }) 40 | return v 41 | } 42 | -------------------------------------------------------------------------------- /lib/limitter/limitter_test.go: -------------------------------------------------------------------------------- 1 | package limitter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sitebatch/waffle-go/lib/limitter" 7 | "github.com/stretchr/testify/assert" 8 | "golang.org/x/time/rate" 9 | ) 10 | 11 | func TestLimitter_Allow(t *testing.T) { 12 | t.Parallel() 13 | 14 | testCases := map[string]struct { 15 | rate int 16 | reqSize int 17 | expect bool 18 | }{ 19 | "If the rate limit is 10req and 10req are sent": { 20 | rate: 10, 21 | reqSize: 9, 22 | expect: true, 23 | }, 24 | "If the rate limit is 10req and 11req are sent": { 25 | rate: 10, 26 | reqSize: 11, 27 | expect: false, 28 | }, 29 | } 30 | 31 | for name, tt := range testCases { 32 | tt := tt 33 | 34 | t.Run(name, func(t *testing.T) { 35 | t.Parallel() 36 | 37 | l := limitter.NewLimitter(rate.Limit(tt.rate), tt.rate) 38 | 39 | for i := 0; i < tt.reqSize; i++ { 40 | l.Allow() 41 | } 42 | 43 | assert.Equal(t, tt.expect, l.Allow()) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/auth/README.md: -------------------------------------------------------------------------------- 1 | # Auth example 2 | 3 | This is the foundation example for [Getting Started] with Waffle. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | $ go run main.go 9 | ``` 10 | 11 | ## Prevent SQL injection 12 | 13 | The `/login` endpoint has a SQL injection vulnerability, though Waffle will block any SQL execution attempts. 14 | 15 | ```shell 16 | $ curl -X POST 'http://localhost:8080/login' \ 17 | --data "email=user@example.com' OR 1=1--&password=password" 18 | request blocked 19 | ``` 20 | 21 | ## Other attack detections 22 | 23 | Requests containing requests or attack payloads from penetration testing tools will be detected. 24 | 25 | ```shell 26 | $ curl 'http://localhost:8080/q?=' 27 | ... 28 | 29 | # Waffle's exporter log output: 30 | 2025/10/31 15:26:01 logger.go:41: "msg"="" "error"="detected xss payload: XSS detected" "detected_at"="2025-10-31 15:26:01.170215 +0900 JST m=+346.052140251" "request_url"="http://localhost:8080/q?=" "rule_id"="xss-attempts" "block"=false "meta"={} 31 | ``` 32 | -------------------------------------------------------------------------------- /internal/inspector/libinjection/sqli_test.go: -------------------------------------------------------------------------------- 1 | package libinjection_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sitebatch/waffle-go/internal/inspector/libinjection" 7 | "github.com/sitebatch/waffle-go/lib" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestIsSQLiPayload(t *testing.T) { 12 | t.Parallel() 13 | 14 | lib.Load() 15 | 16 | testCases := []struct { 17 | value string 18 | expectError bool 19 | }{ 20 | { 21 | value: "test", 22 | expectError: false, 23 | }, 24 | { 25 | value: "-1' and 1=1 union/* foo */select load_file('/etc/passwd')--", 26 | expectError: true, 27 | }, 28 | { 29 | value: "test' # ", 30 | expectError: false, 31 | }, 32 | } 33 | 34 | for _, tc := range testCases { 35 | tc := tc 36 | 37 | t.Run(tc.value, func(t *testing.T) { 38 | t.Parallel() 39 | 40 | err := libinjection.IsSQLiPayload(tc.value) 41 | if tc.expectError { 42 | assert.Error(t, err) 43 | return 44 | } 45 | 46 | assert.NoError(t, err) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/inspector/lfi/lfi.go: -------------------------------------------------------------------------------- 1 | package lfi 2 | 3 | import "regexp" 4 | 5 | var ( 6 | sensitiveFilePaths = []string{ 7 | "/.aws/credentials", 8 | "/.git", 9 | "/.svn", 10 | "/.env", 11 | ".ssh/", 12 | ".bash_history", 13 | "etc/passwd", 14 | "etc/shadow", 15 | "etc/hosts", 16 | "etc/hostname", 17 | "etc/nginx/", 18 | "etc/httpd/", 19 | "etc/apache/", 20 | "etc/apache2/", 21 | "etc/cron.d", 22 | "etc/cron.daily", 23 | "etc/cron.hourly", 24 | "etc/cron.monthly", 25 | "etc/cron.weekly", 26 | "etc/crontab", 27 | "etc/fstab", 28 | "/proc/self", 29 | "/proc/cmdline", 30 | "/proc/environ", 31 | "/root/", 32 | "var/log/", 33 | "var/www/", 34 | "var/run/secrets/", 35 | } 36 | ) 37 | 38 | func IsAttemptDirectoryTraversal(path string) bool { 39 | regexp := regexp.MustCompile(`\.\./`) 40 | return regexp.MatchString(path) 41 | } 42 | 43 | func IsSensitiveFilePath(path string) bool { 44 | for _, p := range sensitiveFilePaths { 45 | regexp := regexp.MustCompile(p) 46 | if regexp.MatchString(path) { 47 | return true 48 | } 49 | } 50 | 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /waf/waf.go: -------------------------------------------------------------------------------- 1 | package waf 2 | 3 | import ( 4 | "github.com/sitebatch/waffle-go/internal/inspector" 5 | "github.com/sitebatch/waffle-go/internal/rule" 6 | ) 7 | 8 | type WAF interface { 9 | // Inspect inspects the given data and returns detection events and an optional block error. 10 | Inspect(data inspector.InspectData) ([]DetectionEvent, error) 11 | } 12 | 13 | type waf struct { 14 | ruleSet *rule.RuleSet 15 | ruleEvaluator *RuleEvaluator 16 | } 17 | 18 | func NewWAF(ruleSet *rule.RuleSet) WAF { 19 | return &waf{ 20 | ruleSet: ruleSet, 21 | ruleEvaluator: NewRuleEvaluator(inspector.NewInspectors()), 22 | } 23 | } 24 | 25 | func (w *waf) Inspect(data inspector.InspectData) ([]DetectionEvent, error) { 26 | var detectionEvents []DetectionEvent 27 | 28 | for _, r := range w.ruleSet.Rules { 29 | results, doBlock := w.ruleEvaluator.Eval(r, data) 30 | 31 | for _, result := range results { 32 | detectionEvents = append(detectionEvents, NewDetectionEvent(data.WafOperationContext, *result)) 33 | } 34 | 35 | if doBlock { 36 | return detectionEvents, &SecurityBlockingError{} 37 | } 38 | } 39 | 40 | return detectionEvents, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/listener/os/file.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "github.com/sitebatch/waffle-go/internal/emitter/os" 5 | "github.com/sitebatch/waffle-go/internal/emitter/waf" 6 | "github.com/sitebatch/waffle-go/internal/inspector" 7 | "github.com/sitebatch/waffle-go/internal/listener" 8 | "github.com/sitebatch/waffle-go/internal/operation" 9 | ) 10 | 11 | type FileSecurity struct{} 12 | 13 | func (f *FileSecurity) Name() string { 14 | return "file_security" 15 | } 16 | 17 | func NewFileSecurity(rootOp operation.Operation) (listener.Listener, error) { 18 | fileSec := &FileSecurity{} 19 | 20 | operation.OnStart(rootOp, fileSec.OnOpen) 21 | operation.OnFinish(rootOp, fileSec.OnFinish) 22 | return fileSec, nil 23 | } 24 | 25 | func (fileSec *FileSecurity) OnOpen(op *os.FileOperation, args os.FileOperationArg) { 26 | op.Run(op, *inspector.NewInspectDataBuilder(op.OperationContext()).WithFileOpenPath(args.Path).Build()) 27 | } 28 | 29 | func (fileSec *FileSecurity) OnFinish(op *os.FileOperation, res *os.FileOperationResult) { 30 | result := &waf.WafOperationResult{} 31 | op.FinishInspect(op, result) 32 | 33 | if result.IsBlock() { 34 | res.BlockErr = result.BlockErr 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/listener/sql/sql.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "github.com/sitebatch/waffle-go/internal/emitter/sql" 5 | "github.com/sitebatch/waffle-go/internal/emitter/waf" 6 | "github.com/sitebatch/waffle-go/internal/inspector" 7 | "github.com/sitebatch/waffle-go/internal/listener" 8 | "github.com/sitebatch/waffle-go/internal/operation" 9 | ) 10 | 11 | type SQLSecurity struct{} 12 | 13 | func (s *SQLSecurity) Name() string { 14 | return "sql_security" 15 | } 16 | 17 | func NewSQLSecurity(rootOp operation.Operation) (listener.Listener, error) { 18 | sqlSec := &SQLSecurity{} 19 | 20 | operation.OnStart(rootOp, sqlSec.OnQueryOrExec) 21 | operation.OnFinish(rootOp, sqlSec.OnFinish) 22 | return sqlSec, nil 23 | } 24 | 25 | func (sqlSec *SQLSecurity) OnQueryOrExec(op *sql.SQLOperation, args sql.SQLOperationArg) { 26 | op.Run(op, *inspector.NewInspectDataBuilder(op.OperationContext()).WithSQLQuery(args.Query).Build()) 27 | } 28 | 29 | func (sqlSec *SQLSecurity) OnFinish(op *sql.SQLOperation, res *sql.SQLOperationResult) { 30 | result := &waf.WafOperationResult{} 31 | op.FinishInspect(op, result) 32 | 33 | if result.IsBlock() { 34 | res.BlockErr = result.BlockErr 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/inspector/sqli/comment.go: -------------------------------------------------------------------------------- 1 | package sqli 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/xwb1989/sqlparser" 9 | ) 10 | 11 | var ( 12 | // This may not be the best approach... 13 | lineCommentPattern = regexp.MustCompile(`(?i)(#|--)\s*(AND|OR|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE)`) 14 | blockCommentPattern = regexp.MustCompile(`(?i)/\*.*?(AND|OR|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE).*?\*/`) 15 | ) 16 | 17 | func IsQueryCommentInjection(query string) error { 18 | comments := extractComments(query) 19 | 20 | for _, comment := range comments { 21 | normalizedValue := strings.TrimSpace(comment) 22 | 23 | if lineCommentPattern.MatchString(normalizedValue) || blockCommentPattern.MatchString(normalizedValue) { 24 | return fmt.Errorf("SQLi detected") 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func extractComments(query string) []string { 32 | var comments []string 33 | tokenizer := sqlparser.NewStringTokenizer(query) 34 | 35 | for { 36 | token, value := tokenizer.Scan() 37 | if token == 0 { 38 | break 39 | } 40 | 41 | if token == sqlparser.COMMENT { 42 | comments = append(comments, string(value)) 43 | } 44 | } 45 | 46 | return comments 47 | } 48 | -------------------------------------------------------------------------------- /internal/inspector/lfi.go: -------------------------------------------------------------------------------- 1 | package inspector 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sitebatch/waffle-go/internal/inspector/lfi" 7 | ) 8 | 9 | type LFIInspector struct{} 10 | 11 | type LFIInspectorArgs struct{} 12 | 13 | func NewLFIInspector() Inspector { 14 | return &LFIInspector{} 15 | } 16 | 17 | func (i *LFIInspector) IsSupportTarget(target InspectTarget) bool { 18 | return target == InspectTargetOSFileOpen 19 | } 20 | 21 | func (i *LFIInspector) Inspect(inspectData InspectData, inspectorArgs InspectorArgs) (*InspectResult, error) { 22 | inspectValue := inspectData.Target[InspectTargetOSFileOpen] 23 | 24 | if inspectValue == nil { 25 | return nil, nil 26 | } 27 | 28 | filePath := inspectValue.GetValue() 29 | 30 | if lfi.IsAttemptDirectoryTraversal(filePath) { 31 | return &InspectResult{ 32 | Target: InspectTargetOSFileOpen, 33 | Payload: filePath, 34 | Message: fmt.Sprintf("detected attempt directory traversal: %s", filePath), 35 | }, nil 36 | } 37 | 38 | if lfi.IsSensitiveFilePath(filePath) { 39 | return &InspectResult{ 40 | Target: InspectTargetOSFileOpen, 41 | Payload: filePath, 42 | Message: fmt.Sprintf("detected sensitive file path access: %s", filePath), 43 | }, nil 44 | } 45 | 46 | return nil, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/inspector/ssrf/matadata_service.go: -------------------------------------------------------------------------------- 1 | package ssrf 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | ) 8 | 9 | var ( 10 | metadataServiceEndpointHosts = []string{ 11 | "metadata.google.internal", 12 | "169.254.169.254", 13 | } 14 | 15 | metadataServiceEndpointCIDRs = []string{ 16 | "169.254.0.0/16", 17 | "fe80::/10", 18 | } 19 | ) 20 | 21 | func IsCloudMetadataServiceURL(u string) error { 22 | parsedURL, err := url.Parse(u) 23 | if err != nil { 24 | return fmt.Errorf("invalid URL: %s", err) 25 | } 26 | 27 | hostname := parsedURL.Hostname() 28 | 29 | for _, h := range metadataServiceEndpointHosts { 30 | if hostname == h { 31 | return fmt.Errorf("cloud metadata service URL detected: %s", u) 32 | } 33 | } 34 | 35 | addrs, err := net.LookupHost(hostname) 36 | if err != nil { 37 | return fmt.Errorf("resolution error: %s", err) 38 | } 39 | 40 | for _, addr := range addrs { 41 | for _, cidr := range metadataServiceEndpointCIDRs { 42 | _, ipNet, err := net.ParseCIDR(cidr) 43 | if err != nil { 44 | return fmt.Errorf("invalid CIDR: %s", err) 45 | } 46 | 47 | if ipNet.Contains(net.ParseIP(addr)) { 48 | return fmt.Errorf("cloud metadata service URL detected: %s", u) 49 | } 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /handler/error.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | 7 | "github.com/sitebatch/waffle-go/internal/log" 8 | ) 9 | 10 | var ( 11 | globalErrorHandlerHolder = defaultErrorHandlerHolder() 12 | delegateErrorHandlerOnce sync.Once 13 | ) 14 | 15 | // ErrorHandler is an interface for handling operation errors. 16 | type ErrorHandler interface { 17 | // HandleError handles any error occurred during operation processing. 18 | HandleError(err error) 19 | } 20 | 21 | func GetErrorHandler() ErrorHandler { 22 | return globalErrorHandlerHolder.Load().(errorHandlerHolder).handler 23 | } 24 | 25 | func SetErrorHandler(handler ErrorHandler) { 26 | delegateErrorHandlerOnce.Do(func() { 27 | globalErrorHandlerHolder.Store( 28 | errorHandlerHolder{handler: handler}, 29 | ) 30 | }) 31 | } 32 | 33 | type ( 34 | errorHandlerHolder struct { 35 | handler ErrorHandler 36 | } 37 | 38 | LogErrorHandler struct{} 39 | ) 40 | 41 | var _ ErrorHandler = (*LogErrorHandler)(nil) 42 | 43 | func (d *LogErrorHandler) HandleError(err error) { 44 | log.Error(err, "failed to process operation") 45 | } 46 | 47 | func defaultErrorHandlerHolder() *atomic.Value { 48 | v := &atomic.Value{} 49 | v.Store(errorHandlerHolder{handler: &LogErrorHandler{}}) 50 | 51 | return v 52 | } 53 | -------------------------------------------------------------------------------- /internal/inspector/libinjection/xss_test.go: -------------------------------------------------------------------------------- 1 | package libinjection_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sitebatch/waffle-go/internal/inspector/libinjection" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsXSSPayload(t *testing.T) { 11 | t.Parallel() 12 | 13 | testCases := []struct { 14 | payload string 15 | detect bool 16 | }{ 17 | { 18 | payload: "", 19 | detect: true, 20 | }, 21 | { 22 | payload: "", 23 | detect: true, 24 | }, 25 | { 26 | payload: "", 27 | detect: true, 28 | }, 29 | { 30 | payload: "", 31 | detect: true, 32 | }, 33 | { 34 | payload: "