Skip to content

Commit 9edd6a0

Browse files
authored
Merge pull request #3 from StudioLambda/fix/critical-security-vulnerabilities
Fix critical security vulnerabilities (XSS, resource leak, OOM)
2 parents 7dfba23 + 3b3a356 commit 9edd6a0

3 files changed

Lines changed: 104 additions & 6 deletions

File tree

contract/request/body.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,42 @@ import (
77
"net/http"
88
)
99

10+
// DefaultMaxBodySize is the default maximum request body size
11+
// (10 MB) used by the size-limited body reading functions.
12+
// This prevents denial-of-service attacks via excessively
13+
// large request bodies that could exhaust server memory.
14+
const DefaultMaxBodySize int64 = 10 << 20 // 10 MB
15+
1016
// Bytes reads the entire request body and returns it as a byte slice.
1117
// The request body is consumed after this call and cannot be read again.
18+
//
19+
// WARNING: This function reads the body without any size limit.
20+
// Prefer [LimitedBytes] or apply [http.MaxBytesReader] in a
21+
// middleware to prevent memory exhaustion from oversized requests.
1222
func Bytes(r *http.Request) ([]byte, error) {
1323
return io.ReadAll(r.Body)
1424
}
1525

26+
// LimitedBytes reads the request body up to maxSize bytes and
27+
// returns it as a byte slice. If the body exceeds maxSize, an
28+
// error is returned. This prevents denial-of-service attacks via
29+
// excessively large request bodies. Pass -1 to use
30+
// [DefaultMaxBodySize].
31+
func LimitedBytes(r *http.Request, maxSize int64) ([]byte, error) {
32+
if maxSize < 0 {
33+
maxSize = DefaultMaxBodySize
34+
}
35+
36+
return io.ReadAll(io.LimitReader(r.Body, maxSize+1))
37+
}
38+
1639
// String reads the request body and returns it as a string.
1740
// It uses [Bytes] internally. The request body is consumed
1841
// after this call and cannot be read again.
42+
//
43+
// WARNING: This function reads the body without any size limit.
44+
// Prefer [LimitedString] or apply [http.MaxBytesReader] in a
45+
// middleware to prevent memory exhaustion from oversized requests.
1946
func String(r *http.Request) (string, error) {
2047
b, err := Bytes(r)
2148

@@ -26,9 +53,26 @@ func String(r *http.Request) (string, error) {
2653
return string(b), nil
2754
}
2855

56+
// LimitedString reads the request body up to maxSize bytes and
57+
// returns it as a string. If the body exceeds maxSize, the result
58+
// is truncated. Pass -1 to use [DefaultMaxBodySize].
59+
func LimitedString(r *http.Request, maxSize int64) (string, error) {
60+
b, err := LimitedBytes(r, maxSize)
61+
62+
if err != nil {
63+
return "", err
64+
}
65+
66+
return string(b), nil
67+
}
68+
2969
// JSON decodes JSON data from the request body into a value of type T.
3070
// It uses a streaming decoder for memory efficiency. The type parameter
3171
// T should match the expected JSON structure.
72+
//
73+
// WARNING: This function decodes without any body size limit.
74+
// Prefer [LimitedJSON] or apply [http.MaxBytesReader] in a
75+
// middleware to prevent memory exhaustion from oversized requests.
3276
func JSON[T any](r *http.Request) (value T, err error) {
3377
if err := json.NewDecoder(r.Body).Decode(&value); err != nil {
3478
return value, err
@@ -37,13 +81,53 @@ func JSON[T any](r *http.Request) (value T, err error) {
3781
return value, nil
3882
}
3983

84+
// LimitedJSON decodes JSON data from the request body into a value
85+
// of type T, reading at most maxSize bytes. This prevents
86+
// denial-of-service attacks via oversized JSON payloads. Pass -1
87+
// to use [DefaultMaxBodySize].
88+
func LimitedJSON[T any](r *http.Request, maxSize int64) (value T, err error) {
89+
if maxSize < 0 {
90+
maxSize = DefaultMaxBodySize
91+
}
92+
93+
limited := io.LimitReader(r.Body, maxSize+1)
94+
95+
if err := json.NewDecoder(limited).Decode(&value); err != nil {
96+
return value, err
97+
}
98+
99+
return value, nil
100+
}
101+
40102
// XML decodes XML data from the request body into a value of type T.
41103
// It uses a streaming decoder for memory efficiency. The type parameter
42104
// T should have appropriate xml struct tags or implement [xml.Unmarshaler].
105+
//
106+
// WARNING: This function decodes without any body size limit.
107+
// Prefer [LimitedXML] or apply [http.MaxBytesReader] in a
108+
// middleware to prevent memory exhaustion from oversized requests.
43109
func XML[T any](r *http.Request) (value T, err error) {
44110
if err := xml.NewDecoder(r.Body).Decode(&value); err != nil {
45111
return value, err
46112
}
47113

48114
return value, nil
49115
}
116+
117+
// LimitedXML decodes XML data from the request body into a value
118+
// of type T, reading at most maxSize bytes. This prevents
119+
// denial-of-service attacks via oversized XML payloads. Pass -1
120+
// to use [DefaultMaxBodySize].
121+
func LimitedXML[T any](r *http.Request, maxSize int64) (value T, err error) {
122+
if maxSize < 0 {
123+
maxSize = DefaultMaxBodySize
124+
}
125+
126+
limited := io.LimitReader(r.Body, maxSize+1)
127+
128+
if err := xml.NewDecoder(limited).Decode(&value); err != nil {
129+
return value, err
130+
}
131+
132+
return value, nil
133+
}

contract/response/static.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package response
33
import (
44
"encoding/json"
55
"encoding/xml"
6+
htmltemplate "html/template"
67
"net/http"
78
"text/template"
89
)
@@ -93,23 +94,24 @@ func StringTemplate(w http.ResponseWriter, status int, tmpl template.Template, d
9394
// - status: The HTTP status code to set
9495
// - data: The HTML string to write to the response
9596
func HTML(w http.ResponseWriter, status int, data string) error {
96-
w.Header().Set("Content-Type", "text/html")
97+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
9798

9899
return Raw(w, status, []byte(data))
99100
}
100101

101102
// HTMLTemplate executes an HTML template with the provided data and writes
102103
// the result as HTML to the response writer. The Content-Type is set to
103-
// text/html. This is the standard way to serve dynamic HTML pages in web
104-
// applications using Go's html/template package.
104+
// text/html; charset=utf-8. This is the standard way to serve dynamic
105+
// HTML pages in web applications using Go's html/template package, which
106+
// provides context-aware escaping to prevent XSS attacks.
105107
//
106108
// Parameters:
107109
// - w: The HTTP response writer
108110
// - status: The HTTP status code to set (note: this is set after template execution)
109-
// - tmpl: The HTML template to execute
111+
// - tmpl: The HTML template to execute (must be html/template for XSS safety)
110112
// - data: The data to pass to the template for execution
111-
func HTMLTemplate(w http.ResponseWriter, status int, tmpl template.Template, data any) error {
112-
w.Header().Set("Content-Type", "text/html")
113+
func HTMLTemplate(w http.ResponseWriter, status int, tmpl htmltemplate.Template, data any) error {
114+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
113115
w.WriteHeader(status)
114116

115117
return tmpl.Execute(w, data)

framework/database/sql.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ func (database *SQL) Exec(ctx context.Context, query string, args ...any) (int64
6161
return 0, err
6262
}
6363

64+
defer q.Close()
65+
6466
result, err := q.ExecContext(ctx, args...)
6567

6668
if err != nil {
@@ -79,6 +81,8 @@ func (database *SQL) ExecNamed(ctx context.Context, query string, arg any) (int6
7981
return 0, err
8082
}
8183

84+
defer q.Close()
85+
8286
result, err := q.ExecContext(ctx, arg)
8387

8488
if err != nil {
@@ -97,6 +101,8 @@ func (database *SQL) Select(ctx context.Context, query string, dest any, args ..
97101
return err
98102
}
99103

104+
defer q.Close()
105+
100106
return q.Select(dest, args...)
101107
}
102108

@@ -110,6 +116,8 @@ func (database *SQL) SelectNamed(ctx context.Context, query string, dest any, ar
110116
return err
111117
}
112118

119+
defer q.Close()
120+
113121
return q.Select(dest, arg)
114122
}
115123

@@ -123,6 +131,8 @@ func (database *SQL) Find(ctx context.Context, query string, dest any, args ...a
123131
return err
124132
}
125133

134+
defer q.Close()
135+
126136
if err := q.GetContext(ctx, dest, args...); err != nil {
127137
if errors.Is(err, sql.ErrNoRows) {
128138
return errors.Join(err, contract.ErrDatabaseNoRows)
@@ -144,6 +154,8 @@ func (database *SQL) FindNamed(ctx context.Context, query string, dest any, arg
144154
return err
145155
}
146156

157+
defer q.Close()
158+
147159
return q.GetContext(ctx, dest, arg)
148160
}
149161

0 commit comments

Comments
 (0)