Skip to content

Commit 7eb6211

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 7fcc48a commit 7eb6211

16 files changed

+118
-0
lines changed

runtime/runtime_single_client.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const (
3333
masterSocket socketType = "master"
3434
)
3535

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

3840
// SingleRuntime handles one runtime API
@@ -180,6 +182,10 @@ func (s *SingleRuntime) ExecuteMaster(command string) (string, error) {
180182
}
181183

182184
func (s *SingleRuntime) executeRaw(command string, retry int, socket socketType) (string, error) {
185+
// Make sure to only execute a single command.
186+
if strings.ContainsAny(command, ";") {
187+
return "", ErrRuntimeInvalidChar
188+
}
183189
result, err := s.readFromSocket(command, socket)
184190
if err != nil && retry > 0 {
185191
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/v6/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
@@ -12204,6 +12204,7 @@ parameters:
1220412204
description: Parent name
1220512205
required: true
1220612206
type: string
12207+
pattern: '^[^\r\n&lt;>*;$#&{}"]+$'
1220712208
full_section:
1220812209
name: full_section
1220912210
in: query
@@ -25597,6 +25598,7 @@ paths:
2559725598
- description: ACL file entry ID
2559825599
in: path
2559925600
name: id
25601+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2560025602
required: true
2560125603
type: string
2560225604
produces:
@@ -25689,6 +25691,7 @@ paths:
2568925691
- description: File entry ID
2569025692
in: path
2569125693
name: id
25694+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2569225695
required: true
2569325696
type: string
2569425697
produces:
@@ -25712,6 +25715,7 @@ paths:
2571225715
- description: File entry ID
2571325716
in: path
2571425717
name: id
25718+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2571525719
required: true
2571625720
type: string
2571725721
produces:
@@ -25796,6 +25800,7 @@ paths:
2579625800
- description: Server name
2579725801
in: path
2579825802
name: name
25803+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2579925804
required: true
2580025805
type: string
2580125806
- $ref: '#/parameters/parent_name'
@@ -25818,6 +25823,7 @@ paths:
2581825823
- description: Server name
2581925824
in: path
2582025825
name: name
25826+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2582125827
required: true
2582225828
type: string
2582325829
- $ref: '#/parameters/parent_name'
@@ -25840,6 +25846,7 @@ paths:
2584025846
- description: Server name
2584125847
in: path
2584225848
name: name
25849+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2584325850
required: true
2584425851
type: string
2584525852
- $ref: '#/parameters/parent_name'
@@ -25912,6 +25919,7 @@ paths:
2591225919
- description: SSL CA file name
2591325920
in: path
2591425921
name: name
25922+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2591525923
required: true
2591625924
type: string
2591725925
responses:
@@ -25931,6 +25939,7 @@ paths:
2593125939
- description: SSL CA file name
2593225940
in: path
2593325941
name: name
25942+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2593425943
required: true
2593525944
type: string
2593625945
produces:
@@ -25956,6 +25965,7 @@ paths:
2595625965
- description: SSL CA file name
2595725966
in: path
2595825967
name: name
25968+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2595925969
required: true
2596025970
type: string
2596125971
- in: formData
@@ -25983,6 +25993,7 @@ paths:
2598325993
- description: SSL CA file name
2598425994
in: path
2598525995
name: name
25996+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2598625997
required: true
2598725998
type: string
2598825999
- description: Payload of the cert entry
@@ -26013,6 +26024,7 @@ paths:
2601326024
- description: SSL CA file name
2601426025
in: path
2601526026
name: name
26027+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2601626028
required: true
2601726029
type: string
2601826030
- description: SSL CA file index
@@ -26085,6 +26097,7 @@ paths:
2608526097
- description: SSL certificate name
2608626098
in: path
2608726099
name: name
26100+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2608826101
required: true
2608926102
type: string
2609026103
responses:
@@ -26104,6 +26117,7 @@ paths:
2610426117
- description: SSL certificate name
2610526118
in: path
2610626119
name: name
26120+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2610726121
required: true
2610826122
type: string
2610926123
produces:
@@ -26127,6 +26141,7 @@ paths:
2612726141
- description: SSL certificate name
2612826142
in: path
2612926143
name: name
26144+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2613026145
required: true
2613126146
type: string
2613226147
- in: formData
@@ -26194,6 +26209,7 @@ paths:
2619426209
- description: CRL file name
2619526210
in: path
2619626211
name: name
26212+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2619726213
required: true
2619826214
type: string
2619926215
responses:
@@ -26213,6 +26229,7 @@ paths:
2621326229
- description: CRL file name
2621426230
in: path
2621526231
name: name
26232+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2621626233
required: true
2621726234
type: string
2621826235
- description: Entry index to return. Starts at 1. If not provided, all entries are returned.
@@ -26244,6 +26261,7 @@ paths:
2624426261
- description: CRL file name
2624526262
in: path
2624626263
name: name
26264+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2624726265
required: true
2624826266
type: string
2624926267
- description: CRL file contents
@@ -26286,11 +26304,13 @@ paths:
2628626304
- description: SSL crt list name
2628726305
in: query
2628826306
name: name
26307+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2628926308
required: true
2629026309
type: string
2629126310
- description: SSL cert entry name
2629226311
in: query
2629326312
name: cert_file
26313+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2629426314
required: true
2629526315
type: string
2629626316
- description: The line number where the entry is located, in case several entries share the same certificate.
@@ -26318,6 +26338,7 @@ paths:
2631826338
- description: SSL crt-list filename
2631926339
in: query
2632026340
name: name
26341+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2632126342
required: true
2632226343
type: string
2632326344
produces:
@@ -26339,6 +26360,7 @@ paths:
2633926360
- description: SSL crt-list filename
2634026361
in: query
2634126362
name: name
26363+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2634226364
required: true
2634326365
type: string
2634426366
- in: body
@@ -26382,6 +26404,7 @@ paths:
2638226404
- description: Stick table name
2638326405
in: path
2638426406
name: name
26407+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2638526408
required: true
2638626409
type: string
2638726410
responses:
@@ -26405,10 +26428,12 @@ paths:
2640526428
- description: A list of filters in format data.<type> <operator> <value> separated by comma
2640626429
in: query
2640726430
name: filter
26431+
pattern: ^[^\r\n;#]+$
2640826432
type: string
2640926433
- description: Key which we want the entries for
2641026434
in: query
2641126435
name: key
26436+
pattern: ^[^\r\n;#]+$
2641226437
type: string
2641326438
- description: Max number of entries to be returned for pagination
2641426439
in: query
@@ -26441,6 +26466,7 @@ paths:
2644126466
data_type:
2644226467
$ref: '#/definitions/stick_table_entry'
2644326468
key:
26469+
pattern: ^[^\r\n;#]+$
2644426470
type: string
2644526471
required:
2644626472
- key
@@ -26485,6 +26511,7 @@ paths:
2648526511
- description: Map file name
2648626512
in: path
2648726513
name: name
26514+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2648826515
required: true
2648926516
type: string
2649026517
- description: If true, deletes file from disk
@@ -26513,6 +26540,7 @@ paths:
2651326540
- description: Map file name
2651426541
in: path
2651526542
name: name
26543+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2651626544
required: true
2651726545
type: string
2651826546
responses:
@@ -26534,6 +26562,7 @@ paths:
2653426562
- description: Map file name
2653526563
in: path
2653626564
name: name
26565+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2653726566
required: true
2653826567
type: string
2653926568
- default: false
@@ -26613,6 +26642,7 @@ paths:
2661326642
- description: Map id
2661426643
in: path
2661526644
name: id
26645+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2661626646
required: true
2661726647
type: string
2661826648
- $ref: '#/parameters/parent_name'
@@ -26638,6 +26668,7 @@ paths:
2663826668
- description: Map id
2663926669
in: path
2664026670
name: id
26671+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2664126672
required: true
2664226673
type: string
2664326674
- $ref: '#/parameters/parent_name'
@@ -26660,6 +26691,7 @@ paths:
2666026691
- description: Map id
2666126692
in: path
2666226693
name: id
26694+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2666326695
required: true
2666426696
type: string
2666526697
- $ref: '#/parameters/parent_name'
@@ -26716,6 +26748,7 @@ paths:
2671626748
- description: Certificate file name
2671726749
in: query
2671826750
name: certificate
26751+
pattern: ^[^\r\n&lt;>*;$#&{}"]+$
2671926752
required: true
2672026753
type: string
2672126754
responses:

specification/haproxy-spec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ parameters:
830830
description: Parent name
831831
required: true
832832
type: string
833+
pattern: '^[^\r\n<>*;$#&{}"]+$'
833834
full_section:
834835
name: full_section
835836
in: query

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ acls_entries_one:
1414
description: File entry ID
1515
required: true
1616
type: string
17+
pattern: '^[^\r\n<>*;$#&{}"]+$'
1718
responses:
1819
'200':
1920
description: Successful operation
@@ -39,6 +40,7 @@ acls_entries_one:
3940
description: File entry ID
4041
required: true
4142
type: string
43+
pattern: '^[^\r\n<>*;$#&{}"]+$'
4244
responses:
4345
'204':
4446
description: Successful operation

specification/paths/runtime/acme.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ acme:
2626
description: Certificate file name
2727
required: true
2828
type: string
29+
pattern: '^[^\r\n<>*;$#&{}"]+$'
2930
responses:
3031
'200':
3132
description: Operation started

specification/paths/runtime/maps.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ maps_one:
3535
description: Map file name
3636
required: true
3737
type: string
38+
pattern: '^[^\r\n<>*;$#&{}"]+$'
3839
responses:
3940
'200':
4041
description: Successful operation
@@ -54,6 +55,7 @@ maps_one:
5455
description: Map file name
5556
required: true
5657
type: string
58+
pattern: '^[^\r\n<>*;$#&{}"]+$'
5759
- name: forceDelete
5860
in: query
5961
description: If true, deletes file from disk
@@ -82,6 +84,7 @@ maps_one:
8284
description: Map file name
8385
required: true
8486
type: string
87+
pattern: '^[^\r\n<>*;$#&{}"]+$'
8588
- name: force_sync
8689
in: query
8790
description: If true, immediately syncs changes to disk

0 commit comments

Comments
 (0)