Skip to content

Commit 86abf24

Browse files
committed
BUG/MEDIUM: runtime: reject special characters in filenames
Avoid command injection when calling the runtime API by rejecting dangerous characters which should never be used in filenames. The rejected characters: \r\n<>*;$#&{}" Note that we cannot simply add quotes around arguments in runtime commands since HAProxy does not parse them, unlike in the configuration file.
1 parent 6d7370c commit 86abf24

File tree

10 files changed

+119
-0
lines changed

10 files changed

+119
-0
lines changed

runtime/runtime_single_client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package runtime
1717

1818
import (
19+
"errors"
1920
"fmt"
2021
"net"
2122
"strings"
@@ -32,6 +33,8 @@ const (
3233
masterSocket socketType = "master"
3334
)
3435

36+
var ErrRuntimeInvalidChar = errors.New("invalid character found in runtime command")
37+
3538
type socketType string
3639

3740
// SingleRuntime handles one runtime API
@@ -174,6 +177,10 @@ func (s *SingleRuntime) ExecuteMaster(command string) (string, error) {
174177
}
175178

176179
func (s *SingleRuntime) executeRaw(command string, retry int, socket socketType) (string, error) {
180+
// Make sure to only execute a single command.
181+
if strings.ContainsAny(command, ";") {
182+
return "", ErrRuntimeInvalidChar
183+
}
177184
result, err := s.readFromSocket(command, socket)
178185
if err != nil && retry > 0 {
179186
retry--
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2026 HAProxy Technologies
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
package runtime_test
17+
18+
import (
19+
"errors"
20+
"testing"
21+
22+
"github.com/haproxytech/client-native/v5/runtime"
23+
)
24+
25+
func TestSingleRuntime_Execute(t *testing.T) {
26+
tests := []struct {
27+
name string // description of this test case
28+
command string
29+
wantErr error
30+
}{
31+
{
32+
name: "two commands with semicolon",
33+
command: "show ssl cert foo;dump ssl foo",
34+
wantErr: runtime.ErrRuntimeInvalidChar,
35+
},
36+
}
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
var s runtime.SingleRuntime
40+
gotErr := s.Execute(tt.command)
41+
if !errors.Is(gotErr, tt.wantErr) {
42+
t.Errorf("got %v, expected %v", gotErr, tt.wantErr)
43+
}
44+
})
45+
}
46+
}

specification/build/haproxy_spec.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18685,6 +18685,7 @@ paths:
1868518685
- description: ACL file entry ID
1868618686
in: path
1868718687
name: id
18688+
pattern: ^[^\r\n<>*;$#&{}"]+$
1868818689
required: true
1868918690
type: string
1869018691
produces:
@@ -18708,6 +18709,7 @@ paths:
1870818709
- description: ACL ID
1870918710
in: query
1871018711
name: acl_id
18712+
pattern: ^[^\r\n<>*;$#&{}"]+$
1871118713
required: true
1871218714
type: string
1871318715
produces:
@@ -18732,6 +18734,7 @@ paths:
1873218734
- description: ACL ID
1873318735
in: query
1873418736
name: acl_id
18737+
pattern: ^[^\r\n<>*;$#&{}"]+$
1873518738
required: true
1873618739
type: string
1873718740
- in: body
@@ -18762,6 +18765,7 @@ paths:
1876218765
- description: ACL ID
1876318766
in: query
1876418767
name: acl_id
18768+
pattern: ^[^\r\n<>*;$#&{}"]+$
1876518769
required: true
1876618770
type: string
1876718771
- in: body
@@ -18788,11 +18792,13 @@ paths:
1878818792
- description: ACL ID
1878918793
in: query
1879018794
name: acl_id
18795+
pattern: ^[^\r\n<>*;$#&{}"]+$
1879118796
required: true
1879218797
type: string
1879318798
- description: File entry ID
1879418799
in: path
1879518800
name: id
18801+
pattern: ^[^\r\n<>*;$#&{}"]+$
1879618802
required: true
1879718803
type: string
1879818804
produces:
@@ -18815,11 +18821,13 @@ paths:
1881518821
- description: ACL ID
1881618822
in: query
1881718823
name: acl_id
18824+
pattern: ^[^\r\n<>*;$#&{}"]+$
1881818825
required: true
1881918826
type: string
1882018827
- description: File entry ID
1882118828
in: path
1882218829
name: id
18830+
pattern: ^[^\r\n<>*;$#&{}"]+$
1882318831
required: true
1882418832
type: string
1882518833
produces:
@@ -18862,6 +18870,7 @@ paths:
1886218870
- description: Parent backend name
1886318871
in: query
1886418872
name: backend
18873+
pattern: ^[^\r\n<>*;$#&{}"]+$
1886518874
required: true
1886618875
type: string
1886718876
responses:
@@ -18881,6 +18890,7 @@ paths:
1888118890
- description: Parent backend name
1888218891
in: query
1888318892
name: backend
18893+
pattern: ^[^\r\n<>*;$#&{}"]+$
1888418894
required: true
1888518895
type: string
1888618896
- in: body
@@ -18912,11 +18922,13 @@ paths:
1891218922
- description: Server name
1891318923
in: path
1891418924
name: name
18925+
pattern: ^[^\r\n<>*;$#&{}"]+$
1891518926
required: true
1891618927
type: string
1891718928
- description: Parent backend name
1891818929
in: query
1891918930
name: backend
18931+
pattern: ^[^\r\n<>*;$#&{}"]+$
1892018932
required: true
1892118933
type: string
1892218934
responses:
@@ -18938,11 +18950,13 @@ paths:
1893818950
- description: Server name
1893918951
in: path
1894018952
name: name
18953+
pattern: ^[^\r\n<>*;$#&{}"]+$
1894118954
required: true
1894218955
type: string
1894318956
- description: Parent backend name
1894418957
in: query
1894518958
name: backend
18959+
pattern: ^[^\r\n<>*;$#&{}"]+$
1894618960
required: true
1894718961
type: string
1894818962
responses:
@@ -18964,11 +18978,13 @@ paths:
1896418978
- description: Server name
1896518979
in: path
1896618980
name: name
18981+
pattern: ^[^\r\n<>*;$#&{}"]+$
1896718982
required: true
1896818983
type: string
1896918984
- description: Parent backend name
1897018985
in: query
1897118986
name: backend
18987+
pattern: ^[^\r\n<>*;$#&{}"]+$
1897218988
required: true
1897318989
type: string
1897418990
- in: body
@@ -19017,6 +19033,7 @@ paths:
1901719033
- description: Stick table name
1901819034
in: path
1901919035
name: name
19036+
pattern: ^[^\r\n<>*;$#&{}"]+$
1902019037
required: true
1902119038
type: string
1902219039
- description: Process number if master-worker mode, if not only first process is returned
@@ -19044,6 +19061,7 @@ paths:
1904419061
- description: Stick table name
1904519062
in: query
1904619063
name: stick_table
19064+
pattern: ^[^\r\n<>*;$#&{}"]+$
1904719065
required: true
1904819066
type: string
1904919067
- description: Process number if master-worker mode, if not only first process is returned
@@ -19054,10 +19072,12 @@ paths:
1905419072
- description: A list of filters in format data.<type> <operator> <value> separated by comma
1905519073
in: query
1905619074
name: filter
19075+
pattern: ^[^\r\n;#]+$
1905719076
type: string
1905819077
- description: Key which we want the entries for
1905919078
in: query
1906019079
name: key
19080+
pattern: ^[^\r\n;#]+$
1906119081
type: string
1906219082
- description: Max number of entries to be returned for pagination
1906319083
in: query
@@ -19084,6 +19104,7 @@ paths:
1908419104
- description: Stick table name
1908519105
in: query
1908619106
name: stick_table
19107+
pattern: ^[^\r\n<>*;$#&{}"]+$
1908719108
required: true
1908819109
type: string
1908919110
- description: Process number if master-worker mode, if not only first process is returned
@@ -19099,6 +19120,7 @@ paths:
1909919120
data_type:
1910019121
$ref: '#/definitions/stick_table_entry'
1910119122
key:
19123+
pattern: ^[^\r\n;#]+$
1910219124
type: string
1910319125
required:
1910419126
- key
@@ -19143,6 +19165,7 @@ paths:
1914319165
- description: Map file name
1914419166
in: path
1914519167
name: name
19168+
pattern: ^[^\r\n<>*;$#&{}"]+$
1914619169
required: true
1914719170
type: string
1914819171
- description: If true, deletes file from disk
@@ -19171,6 +19194,7 @@ paths:
1917119194
- description: Map file name
1917219195
in: path
1917319196
name: name
19197+
pattern: ^[^\r\n<>*;$#&{}"]+$
1917419198
required: true
1917519199
type: string
1917619200
responses:
@@ -19192,6 +19216,7 @@ paths:
1919219216
- description: Map file name
1919319217
in: path
1919419218
name: name
19219+
pattern: ^[^\r\n<>*;$#&{}"]+$
1919519220
required: true
1919619221
type: string
1919719222
- default: false
@@ -19224,6 +19249,7 @@ paths:
1922419249
- description: Mapfile attribute storage_name
1922519250
in: query
1922619251
name: map
19252+
pattern: ^[^\r\n<>*;$#&{}"]+$
1922719253
required: true
1922819254
type: string
1922919255
responses:
@@ -19245,6 +19271,7 @@ paths:
1924519271
- description: Mapfile attribute storage_name
1924619272
in: query
1924719273
name: map
19274+
pattern: ^[^\r\n<>*;$#&{}"]+$
1924819275
required: true
1924919276
type: string
1925019277
- default: false
@@ -19279,11 +19306,13 @@ paths:
1927919306
- description: Map id
1928019307
in: path
1928119308
name: id
19309+
pattern: ^[^\r\n<>*;$#&{}"]+$
1928219310
required: true
1928319311
type: string
1928419312
- description: Mapfile attribute storage_name
1928519313
in: query
1928619314
name: map
19315+
pattern: ^[^\r\n<>*;$#&{}"]+$
1928719316
required: true
1928819317
type: string
1928919318
- default: false
@@ -19308,11 +19337,13 @@ paths:
1930819337
- description: Map id
1930919338
in: path
1931019339
name: id
19340+
pattern: ^[^\r\n<>*;$#&{}"]+$
1931119341
required: true
1931219342
type: string
1931319343
- description: Mapfile attribute storage_name
1931419344
in: query
1931519345
name: map
19346+
pattern: ^[^\r\n<>*;$#&{}"]+$
1931619347
required: true
1931719348
type: string
1931819349
responses:
@@ -19334,11 +19365,13 @@ paths:
1933419365
- description: Map id
1933519366
in: path
1933619367
name: id
19368+
pattern: ^[^\r\n<>*;$#&{}"]+$
1933719369
required: true
1933819370
type: string
1933919371
- description: Mapfile attribute storage_name
1934019372
in: query
1934119373
name: map
19374+
pattern: ^[^\r\n<>*;$#&{}"]+$
1934219375
required: true
1934319376
type: string
1934419377
- default: false

specification/paths/runtime/acls.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ acls_one:
2828
description: ACL file entry ID
2929
required: true
3030
type: string
31+
pattern: '^[^\r\n<>*;$#&{}"]+$'
3132
responses:
3233
'200':
3334
description: Successful operation

specification/paths/runtime/acls_entries.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ acls_entries_one:
1313
description: ACL ID
1414
required: true
1515
type: string
16+
pattern: '^[^\r\n<>*;$#&{}"]+$'
1617
- name: id
1718
in: path
1819
description: File entry ID
1920
required: true
2021
type: string
22+
pattern: '^[^\r\n<>*;$#&{}"]+$'
2123
responses:
2224
'200':
2325
description: Successful operation
@@ -42,11 +44,13 @@ acls_entries_one:
4244
description: ACL ID
4345
required: true
4446
type: string
47+
pattern: '^[^\r\n<>*;$#&{}"]+$'
4548
- name: id
4649
in: path
4750
description: File entry ID
4851
required: true
4952
type: string
53+
pattern: '^[^\r\n<>*;$#&{}"]+$'
5054
responses:
5155
'204':
5256
description: Successful operation
@@ -70,6 +74,7 @@ acls_entries:
7074
description: ACL ID
7175
required: true
7276
type: string
77+
pattern: '^[^\r\n<>*;$#&{}"]+$'
7378
- name: data
7479
in: body
7580
required: true
@@ -99,6 +104,7 @@ acls_entries:
99104
description: ACL ID
100105
required: true
101106
type: string
107+
pattern: '^[^\r\n<>*;$#&{}"]+$'
102108
responses:
103109
'200':
104110
description: Successful operation
@@ -120,6 +126,7 @@ acls_entries:
120126
description: ACL ID
121127
required: true
122128
type: string
129+
pattern: '^[^\r\n<>*;$#&{}"]+$'
123130
- name: data
124131
required: true
125132
in: body

0 commit comments

Comments
 (0)