Skip to content

Commit de5bc2e

Browse files
authored
feat: implement decodeBase64 filter for header value decoding (#4100)
1 parent 3d2c5a7 commit de5bc2e

5 files changed

Lines changed: 622 additions & 0 deletions

File tree

docs/reference/filters.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,24 @@ PathSubtree("/") && Method("OPTIONS")
338338
-> <shunt>;
339339
```
340340

341+
### decodeBase64RequestHeader
342+
343+
Decodes the value of a chosen request header by decoding base64. It
344+
supports partial value decoding by the optional second argument. One
345+
known use case is if client sends you a bearer token as user of a
346+
basic auth header, because of client limitations.
347+
348+
Example:
349+
```
350+
decodeBase64RequestHeader("X-Foo")
351+
decodeBase64RequestHeader("X-Foo", 2)
352+
decodeBase64RequestHeader("Authorization", 1) //
353+
```
354+
355+
### decodeBase64ResponseHeader
356+
357+
Similar to [decodeBase64RequestHeader](#decodeBase64RequestHeader) on response headers.
358+
341359
### encodeRequestHeader
342360

343361
The filter has 2 arguments, the header name and the encoding.

filters/builtin/builtin.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ func Filters() []filters.Spec {
144144
NewModResponseHeader(),
145145
NewEncodeRequestHeader(),
146146
NewEncodeResponseHeader(),
147+
NewDecodeRequestHeaderBase64(),
148+
NewDecodeResponseHeaderBase64(),
147149
NewDropQuery(),
148150
NewSetQuery(),
149151
NewHealthCheck(),

filters/builtin/decode_base64.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package builtin
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/zalando/skipper/filters"
9+
)
10+
11+
type decodeBase64Type int
12+
13+
const (
14+
decodeRequestHeaderBase64 decodeBase64Type = iota
15+
decodeResponseHeaderBase64
16+
)
17+
18+
// decodeBase64 filter for decoding base64-encoded parts of headers
19+
type decodeBase64 struct {
20+
typ decodeBase64Type
21+
headerName string
22+
partIndex *int // nil means decode entire value, otherwise decode the part at this index
23+
}
24+
25+
// NewDecodeRequestHeaderBase64 returns a filter specification that decodes
26+
// base64-encoded request header values or specific parts of them.
27+
// Instances expect one or two parameters:
28+
// - First parameter: header name (required)
29+
// - Second parameter: part index (optional, 0-based)
30+
// If the second parameter is not provided, the entire header value is decoded.
31+
// Name: "decodeRequestHeaderBase64".
32+
func NewDecodeRequestHeaderBase64() filters.Spec {
33+
return &decodeBase64{typ: decodeRequestHeaderBase64}
34+
}
35+
36+
// NewDecodeResponseHeaderBase64 returns a filter specification that decodes
37+
// base64-encoded response header values or specific parts of them.
38+
// Instances expect one or two parameters:
39+
// - First parameter: header name (required)
40+
// - Second parameter: part index (optional, 0-based)
41+
// If the second parameter is not provided, the entire header value is decoded.
42+
// Name: "decodeResponseHeaderBase64".
43+
func NewDecodeResponseHeaderBase64() filters.Spec {
44+
return &decodeBase64{typ: decodeResponseHeaderBase64}
45+
}
46+
47+
func (spec *decodeBase64) Name() string {
48+
switch spec.typ {
49+
case decodeRequestHeaderBase64:
50+
return filters.DecodeBase64RequestHeaderName
51+
case decodeResponseHeaderBase64:
52+
return filters.DecodeBase64ResponseHeaderName
53+
default:
54+
panic("invalid decodeBase64 type")
55+
}
56+
}
57+
58+
//lint:ignore ST1016 "spec" makes sense here and we reuse the type for the filter
59+
func (spec *decodeBase64) CreateFilter(args []any) (filters.Filter, error) {
60+
if len(args) < 1 || len(args) > 2 {
61+
return nil, filters.ErrInvalidFilterParameters
62+
}
63+
64+
headerName, ok := args[0].(string)
65+
if !ok {
66+
return nil, filters.ErrInvalidFilterParameters
67+
}
68+
69+
var partIndex *int
70+
if len(args) == 2 {
71+
// Second parameter is optional and should be a number representing the part index
72+
switch v := args[1].(type) {
73+
case float64:
74+
idx := int(v)
75+
partIndex = &idx
76+
default:
77+
return nil, filters.ErrInvalidFilterParameters
78+
}
79+
}
80+
81+
return &decodeBase64{
82+
typ: spec.typ,
83+
headerName: headerName,
84+
partIndex: partIndex,
85+
}, nil
86+
}
87+
88+
// decodeBase64Value decodes a base64-encoded string or a specific part of it.
89+
// If partIndex is nil, it decodes the entire value.
90+
// If partIndex is provided, it splits the value by spaces and decodes the part at that index.
91+
func decodeBase64Value(value string, partIndex *int) (string, error) {
92+
if partIndex == nil {
93+
// Decode the entire value
94+
decoded, err := base64.StdEncoding.DecodeString(value)
95+
if err != nil {
96+
return "", fmt.Errorf("failed to decode base64: %w", err)
97+
}
98+
return string(decoded), nil
99+
}
100+
101+
// Split by spaces and decode the specified part
102+
parts := strings.Fields(value)
103+
if *partIndex < 0 || *partIndex >= len(parts) {
104+
return "", fmt.Errorf("part index %d out of range (header has %d parts)", *partIndex, len(parts))
105+
}
106+
107+
decoded, err := base64.StdEncoding.DecodeString(parts[*partIndex])
108+
if err != nil {
109+
return "", fmt.Errorf("failed to decode base64 part at index %d: %w", *partIndex, err)
110+
}
111+
112+
// Reconstruct the header with the decoded part
113+
parts[*partIndex] = string(decoded)
114+
return strings.Join(parts, " "), nil
115+
}
116+
117+
func (f *decodeBase64) Request(ctx filters.FilterContext) {
118+
if f.typ != decodeRequestHeaderBase64 {
119+
return
120+
}
121+
122+
header := ctx.Request().Header
123+
headerValue := header.Get(f.headerName)
124+
if headerValue == "" {
125+
return
126+
}
127+
128+
decoded, err := decodeBase64Value(headerValue, f.partIndex)
129+
if err != nil {
130+
// Log error but continue - invalid base64 shouldn't break the request
131+
ctx.Logger().Warnf("decodeBase64 filter error for header %s: %v", f.headerName, err)
132+
return
133+
}
134+
135+
header.Set(f.headerName, decoded)
136+
}
137+
138+
func (f *decodeBase64) Response(ctx filters.FilterContext) {
139+
if f.typ != decodeResponseHeaderBase64 {
140+
return
141+
}
142+
143+
header := ctx.Response().Header
144+
headerValue := header.Get(f.headerName)
145+
if headerValue == "" {
146+
return
147+
}
148+
149+
decoded, err := decodeBase64Value(headerValue, f.partIndex)
150+
if err != nil {
151+
// Log error but continue - invalid base64 shouldn't break the response
152+
ctx.Logger().Warnf("decodeBase64 filter error for header %s: %v", f.headerName, err)
153+
return
154+
}
155+
156+
header.Set(f.headerName, decoded)
157+
}

0 commit comments

Comments
 (0)