Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Sentinel's Journal

## 2026-10-08 - Added CORS headers protection
**Vulnerability:** CRLF Injection in headers via Environment Variables (HTTP Response Splitting).
**Learning:** `responder.Render()` expands `${VAR}` in header values using `r.env.Expand(h.Value)`. However, the environment variable values themselves are not sanitized! `Spec.Validate()` only checks the literal header values in the config/CLI before expansion (`validHeaderValue`). If an environment variable like `PORTBELL_API_KEY` contains `\r\n` (CRLF), the expanded value will be injected into the raw HTTP response, leading to HTTP Response Splitting.
**Prevention:** Always validate expanded values, or strip `\r` and `\n` from environment variables during expansion in headers.
19 changes: 18 additions & 1 deletion internal/responder/responder.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,29 @@ func (r *Responder) Set(s Spec) error {
return nil
}

func sanitizeHeaderValue(v string) string {
if validHeaderValue(v) {
return v
}
var b strings.Builder
b.Grow(len(v))
for i := 0; i < len(v); i++ {
c := v[i]
if c == '\t' || (c >= 0x20 && c != 0x7f) {
b.WriteByte(c)
} else {
b.WriteByte(' ')
}
}
return b.String()
}

// Render returns the reply to send now, with placeholders expanded.
func (r *Responder) Render() events.Response {
s := r.Get()
headers := make([]events.Header, len(s.Headers))
for i, h := range s.Headers {
headers[i] = events.Header{Name: h.Name, Value: r.env.Expand(h.Value)}
headers[i] = events.Header{Name: h.Name, Value: sanitizeHeaderValue(r.env.Expand(h.Value))}
}
return events.Response{
Status: s.Status,
Expand Down
18 changes: 18 additions & 0 deletions internal/responder/responder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,21 @@ func TestFormatParseRoundTrip(t *testing.T) {
t.Fatalf("round trip mismatch: %+v", back)
}
}

func TestResponderRenderSanitizesHeaders(t *testing.T) {
env := map[string]string{"PORTBELL_INJECT": "val\r\nInjected: 1"}
p := EnvPolicy{
Prefix: "PORTBELL_",
Lookup: func(k string) (string, bool) { v, ok := env[k]; return v, ok },
}
r := New(Spec{
Status: 200,
Headers: []events.Header{{Name: "X-Test", Value: "${PORTBELL_INJECT}"}},
Body: "body",
}, p)

out := r.Render()
if out.Headers[0].Value != "val Injected: 1" {
t.Fatalf("expected sanitized header, got %q", out.Headers[0].Value)
}
}
Loading