Introduction
Web Application Firewalls (WAF) have long been a standard security solution for protecting web applications. Cloud-based WAFs like AWS WAF and Cloudflare WAF are particularly popular due to their ease of implementation. However, they come with several challenges:
- Limited understanding of application context
- High rate of false positives
- Restricted custom logic implementation
To address these challenges, a new approach called In-app WAF or RASP (Runtime Application Self-Protection) has been gaining attention.
In this post, I'll introduce Waffle, a library for integrating In-app WAF capabilities into Go web applications.
What is In-app WAF / RASP?
In-app WAF/RASP is not meant to replace existing cloud WAFs but rather to complement them by embedding WAF functionality directly into your application for enhanced protection.
It can handle common web application attacks like SQL injection and XSS, as well as application business logic attacks such as credential stuffing and brute force attempts.
The key advantage is accurate detection and prevention through complete request context awareness.
Consider this HTTP request for creating a blog post:
POST /blog/post HTTP/1.1
...
{
"title": "What is SQL ?"
"body": "SQL example code: `SELECT * FROM users` ..."
}
If your application uses placeholders to safely construct SQL statements, SQL injection isn't possible. However, cloud-based WAFs that rely on pattern matching would block this request because it contains suspicious SQL-like patterns (the string SELECT * FROM
raises SQL injection concerns).
Developers often find themselves tediously adjusting parameters, endpoints, or WAF rules to reduce these false positives. What a cumbersome task!
In contrast, In-app WAF / RASP understands the request context. It recognizes when placeholders aren't being used and only blocks attacks when "SQL injection is actually possible." This context-aware approach results in fewer false positives and can even help mitigate zero-day vulnerabilities.
Implementing In-App WAF / RASP with Waffle in Go Applications
Waffle is a library that enables In-App WAF / RASP functionality in Go web applications.
Let's see how to integrate Waffle into your application and how it prevents attacks.
Example Application
While this example uses the standard library's net/http
, Waffle also supports other libraries like Gin and GORM.
For more details, check out the Supported Libraries documentation.
The following application has a SQL injection vulnerability in the /login
endpoint:
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
_ "github.com/mattn/go-sqlite3"
)
var database *sql.DB
func init() {
setupDB()
}
func newHTTPHandler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/login", http.HandlerFunc(loginController))
return mux
}
func main() {
srv := &http.Server{
Addr: ":8000",
Handler: newHTTPHandler(),
}
srv.ListenAndServe()
}
func loginController(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
password := r.FormValue("password")
if err := login(r.Context(), email, password); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("Login success"))
}
func login(ctx context.Context, email, password string) error {
// ⚠️ SQL INJECTION VULNERABILITY
rows, err := database.QueryContext(ctx, fmt.Sprintf("SELECT * FROM users WHERE email = '%s' AND password = '%s'", email, password))
if err != nil {
return err
}
defer rows.Close()
if !rows.Next() {
return fmt.Errorf("invalid email or password")
}
// do something
return nil
}
func setupDB() {
db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
if err != nil {
panic(err)
}
if _, err := db.Exec("CREATE TABLE users(id int, email text, password text);"); err != nil {
panic(err)
}
if _, err := db.Exec("INSERT INTO users(id, email, password) VALUES(1, 'user@example.com', 'password');"); err != nil {
panic(err)
}
database = db
}
$ go run .
# SQL injection attack
$ curl -i -X POST 'http://localhost:8000/login' \
--data "email=user@example.com' OR 1=1--&password="
HTTP/1.1 200 OK
Date: Sun, 05 Jan 2025 10:32:50 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Login success
Integrating Waffle to prevent SQL injection
Let's integrate Waffle to prevent SQL injection:
$ go get github.com/sitebatch/waffle-go
Modify main.go
as follows:
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/sitebatch/waffle-go"
"github.com/sitebatch/waffle-go/action"
waffleSQL "github.com/sitebatch/waffle-go/contrib/database/sql"
waffleHTTP "github.com/sitebatch/waffle-go/contrib/net/http"
_ "github.com/mattn/go-sqlite3"
)
var database *sql.DB
func init() {
setupDB()
}
func newHTTPHandler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/login", http.HandlerFunc(loginController))
handler := waffleHTTP.WafMiddleware(mux)
return handler
}
func main() {
srv := &http.Server{
Addr: ":8000",
Handler: newHTTPHandler(),
}
// Start waffle with debug mode
waffle.Start(waffle.WithDebug())
srv.ListenAndServe()
}
func loginController(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
password := r.FormValue("password")
if err := login(r.Context(), email, password); err != nil {
var actionErr *action.BlockError
if errors.As(err, &actionErr) {
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("Login success"))
}
func login(ctx context.Context, email, password string) error {
// ⚠️ SQL INJECTION VULNERABILITY
rows, err := database.QueryContext(ctx, fmt.Sprintf("SELECT * FROM users WHERE email = '%s' AND password = '%s'", email, password))
if err != nil {
return err
}
defer rows.Close()
if !rows.Next() {
return fmt.Errorf("invalid email or password")
}
// do something
return nil
}
func setupDB() {
db, err := waffleSQL.Open("sqlite3", "file::memory:?cache=shared")
if err != nil {
panic(err)
}
if _, err := db.Exec("CREATE TABLE users(id int, email text, password text);"); err != nil {
panic(err)
}
if _, err := db.Exec("INSERT INTO users(id, email, password) VALUES(1, 'user@example.com', 'password');"); err != nil {
panic(err)
}
database = db
}
The changes are minimal:
diff --git a/main.go b/main.go
index 90b8197..9fefb06 100644
--- a/main.go
+++ b/main.go
@@ -3,9 +3,15 @@ package main
import (
"context"
"database/sql"
+ "errors"
"fmt"
"net/http"
+ "github.com/sitebatch/waffle-go"
+ "github.com/sitebatch/waffle-go/action"
+ waffleSQL "github.com/sitebatch/waffle-go/contrib/database/sql"
+ waffleHTTP "github.com/sitebatch/waffle-go/contrib/net/http"
+
_ "github.com/mattn/go-sqlite3"
)
@@ -19,7 +25,9 @@ func newHTTPHandler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/login", http.HandlerFunc(loginController))
- return mux
+ handler := waffleHTTP.WafMiddleware(mux)
+
+ return handler
}
func main() {
@@ -28,6 +36,9 @@ func main() {
Handler: newHTTPHandler(),
}
+ // Start waffle with debug mode
+ waffle.Start(waffle.WithDebug())
+
srv.ListenAndServe()
}
@@ -36,6 +47,11 @@ func loginController(w http.ResponseWriter, r *http.Request) {
password := r.FormValue("password")
if err := login(r.Context(), email, password); err != nil {
+ var actionErr *action.BlockError
+ if errors.As(err, &actionErr) {
+ return
+ }
+
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -60,7 +76,7 @@ func login(ctx context.Context, email, password string) error {
}
func setupDB() {
- db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
+ db, err := waffleSQL.Open("sqlite3", "file::memory:?cache=shared")
if err != nil {
panic(err)
}
Now when we try a SQL injection attack, Waffle blocks it:
$ curl -i -X POST 'http://localhost:8000/login' \
--data "email=user@example.com' OR 1=1--&password=" -i
HTTP/1.1 403 Forbidden
Date: Sun, 05 Jan 2025 10:38:22 GMT
Content-Length: 1574
Content-Type: text/html; charset=utf-8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Access Denied</title>
This HTML is the error message returned by default by waffle and looks like this:
If using placeholders:
When using placeholders, Waffle recognizes that SQL injection isn't possible and won't block the request:
# Fix SQL injection vulnerability
diff --git a/main.go
b/main.go
index 9fefb06..5b482f2 100644
--- a/main.go
+++ b/main.go
@@ -60,7 +60,7 @@ func loginController(w http.ResponseWriter, r *http.Request) {
}
func login(ctx context.Context, email, password string) error {
- rows, err := database.QueryContext(ctx, fmt.Sprintf("SELECT * FROM users WHERE email = '%s' AND password = '%s'", email, password))
+ rows, err := database.QueryContext(ctx, "SELECT * FROM users WHERE email = ? AND password = ?", email, password)
if err != nil {
return err
}
# Waffle won't block the request since SQL injection isn't possible
$ curl -i -X POST 'http://localhost:8000/login' \
--data "email=user@example.com' OR 1=1--&password="
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sun, 05 Jan 2025 10:49:05 GMT
Content-Length: 26
invalid email or password
Note that even in this case, Waffle can still detect attempted SQL injection like a cloud-based WAF (though it won't block it):
# on application logs
...
time=2025-01-05T19:49:05.418+09:00 level=INFO msg="[waffle] Threat detected: detected sql injection payload: SQLi detected" ruleID=sql-injection-attempts inspector=libinjection_sqli clientIP=[::1]:58614 url=http://localhost:8000/login
Attacks Detected and Prevented by Waffle
While we've demonstrated SQL injection prevention, Waffle can detect and prevent various attacks:
- Reconnaissance by known security scanners
- Directory traversal
- XSS
- SQL injection
- Sensitive file access
- SSRF
- Account takeover
For more details, check out the Rule List documentation.
Rules are continuously updated, and contributions are welcome.
Conclusion
By integrating Waffle into your application, you can accurately detect and prevent attacks.
For framework-specific implementation guides and detailed usage instructions, refer to the Guides section in the documentation.
Waffle is under active development. We welcome feedback and contributions.
Top comments (0)