Skip to content

Commit 40d747d

Browse files
Reject user texts matching a regex (#88)
Sometimes users send something they really should not in their rageshake descriptions. I propose here an approach to reject rageshakes whose description matches a regex. The rejection reason is reflected client-side so the user's client can show them want went wrong. The sample config file provides a regex to reject user texts that could contain a recovery key. Co-authored-by: Richard van der Hoff <[email protected]>
1 parent 1d0a896 commit 40d747d

File tree

6 files changed

+112
-54
lines changed

6 files changed

+112
-54
lines changed

changelog.d/88.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reject user text (problem description) matching a regex and send the reason why to the client-side.

docs/blocked_rageshake.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ The rageshake server you attempted to upload a report to is not accepting ragesh
1414
Generally, the developers who run a rageshake server will only be able to handle reports for applications they are developing,
1515
and your application is not listed as one of those applications.
1616

17+
The rageshake server could also be rejecting the text you wrote in your bug report because its content matches a rejection rule. This usually happens to prevent you from disclosing private information in the bug report itself.
18+
1719
Please contact the distributor of your application or the administrator of the web site you visit to report this as a problem.
1820

1921
## For developers of matrix clients

main.go

+55-42
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"net"
2828
"net/http"
2929
"os"
30+
"regexp"
3031
"strings"
3132
"text/template"
3233
"time"
@@ -97,55 +98,76 @@ type config struct {
9798
GenericWebhookURLs []string `yaml:"generic_webhook_urls"`
9899
}
99100

100-
// RejectionCondition contains the fields that should match a bug report for it to be rejected.
101+
// RejectionCondition contains the fields that can match a bug report for it to be rejected.
102+
// All the (optional) fields must match for the rejection condition to apply
101103
type RejectionCondition struct {
102-
// Required field: if a payload does not match this app name, the condition does not match.
104+
// App name, applies only if not empty
103105
App string `yaml:"app"`
104-
// Optional: version that must also match in addition to the app and label. If empty, does not check version.
106+
// Version, applies only if not empty
105107
Version string `yaml:"version"`
106-
// Optional: label that must also match in addition to the app and version. If empty, does not check label.
108+
// Label, applies only if not empty
107109
Label string `yaml:"label"`
110+
// Message sent by the user, applies only if not empty
111+
UserTextMatch string `yaml:"usertext"`
112+
// Send this text to the client-side to inform the user why the server rejects the rageshake. Uses a default generic value if empty.
113+
Reason string `yaml:"reason"`
108114
}
109115

110-
// shouldReject returns true if the app name AND version AND labels all match the rejection condition.
111-
// If any one of these do not match the condition, it is not rejected.
112-
func (c RejectionCondition) shouldReject(appName, version string, labels []string) bool {
113-
if appName != c.App {
114-
return false
115-
}
116-
// version was a condition and it doesn't match => accept it
117-
if version != c.Version && c.Version != "" {
118-
return false
116+
func (c RejectionCondition) matchesApp(p *payload) bool {
117+
// Empty `RejectionCondition.App` is a wildcard which matches anything
118+
return c.App == "" || c.App == p.AppName
119+
}
120+
121+
func (c RejectionCondition) matchesVersion(p *payload) bool {
122+
version := ""
123+
if p.Data != nil {
124+
version = p.Data["Version"]
119125
}
126+
// Empty `RejectionCondition.Version` is a wildcard which matches anything
127+
return c.Version == "" || c.Version == version
128+
}
120129

121-
// label was a condition and no label matches it => accept it
122-
if c.Label != "" {
123-
labelMatch := false
124-
for _, l := range labels {
125-
if l == c.Label {
126-
labelMatch = true
127-
break
128-
}
129-
}
130-
if !labelMatch {
131-
return false
130+
func (c RejectionCondition) matchesLabel(p *payload) bool {
131+
// Empty `RejectionCondition.Label` is a wildcard which matches anything
132+
if c.Label == "" {
133+
return true
134+
}
135+
// Otherwise return true only if there is a label that matches
136+
labelMatch := false
137+
for _, l := range p.Labels {
138+
if l == c.Label {
139+
labelMatch = true
140+
break
132141
}
133142
}
143+
return labelMatch
144+
}
134145

135-
return true
146+
func (c RejectionCondition) matchesUserText(p *payload) bool {
147+
// Empty `RejectionCondition.UserTextMatch` is a wildcard which matches anything
148+
return c.UserTextMatch == "" || regexp.MustCompile(c.UserTextMatch).MatchString(p.UserText)
136149
}
137150

138-
func (c *config) matchesRejectionCondition(p *payload) bool {
139-
for _, rc := range c.RejectionConditions {
140-
version := ""
141-
if p.Data != nil {
142-
version = p.Data["Version"]
151+
func (c RejectionCondition) shouldReject(p *payload) *string {
152+
if c.matchesApp(p) && c.matchesVersion(p) && c.matchesLabel(p) && c.matchesUserText(p) {
153+
// RejectionCondition matches all of the conditions: we should reject this submission/
154+
defaultReason := "app or user text rejected"
155+
if c.Reason != "" {
156+
return &c.Reason
143157
}
144-
if rc.shouldReject(p.AppName, version, p.Labels) {
145-
return true
158+
return &defaultReason
159+
}
160+
return nil
161+
}
162+
163+
func (c *config) matchesRejectionCondition(p *payload) *string {
164+
for _, rc := range c.RejectionConditions {
165+
reject := rc.shouldReject(p)
166+
if reject != nil {
167+
return reject
146168
}
147169
}
148-
return false
170+
return nil
149171
}
150172

151173
func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
@@ -319,14 +341,5 @@ func loadConfig(configPath string) (*config, error) {
319341
if err = yaml.Unmarshal(contents, &cfg); err != nil {
320342
return nil, err
321343
}
322-
// sanity check rejection conditions
323-
for _, rc := range cfg.RejectionConditions {
324-
if rc.App == "" {
325-
fmt.Println("rejection_condition missing an app field so will never match anything.")
326-
}
327-
if rc.Label == "" && rc.Version == "" {
328-
fmt.Println("rejection_condition missing both label and version so will always match, specify label and/or version")
329-
}
330-
}
331344
return &cfg, nil
332345
}

main_test.go

+44-7
Original file line numberDiff line numberDiff line change
@@ -17,54 +17,86 @@ func TestConfigRejectionCondition(t *testing.T) {
1717
App: "my-app",
1818
Version: "0.1.2",
1919
Label: "nightly",
20+
Reason: "no nightlies",
2021
},
2122
{
2223
App: "block-my-app",
2324
},
25+
{
26+
UserTextMatch: "(\\w{4}\\s){11}\\w{4}",
27+
Reason: "it matches a recovery key and recovery keys are private",
28+
},
2429
},
2530
}
2631
rejectPayloads := []payload{
2732
{
2833
AppName: "my-app",
2934
Data: map[string]string{
3035
"Version": "0.1.0",
36+
// Hack add how we expect the rageshake to be rejected to the test
37+
// The actual data in a rageshake has no ExpectedRejectReason field
38+
"ExpectedRejectReason": "app or user text rejected",
3139
},
3240
},
3341
{
3442
AppName: "my-app",
35-
Data: map[string]string{},
36-
Labels: []string{"0.1.1"},
43+
Data: map[string]string{
44+
"ExpectedRejectReason": "app or user text rejected",
45+
},
46+
Labels: []string{"0.1.1"},
3747
},
3848
{
3949
AppName: "my-app",
4050
Labels: []string{"foo", "nightly"},
4151
Data: map[string]string{
42-
"Version": "0.1.2",
52+
"Version": "0.1.2",
53+
"ExpectedRejectReason": "no nightlies",
4354
},
4455
},
4556
{
4657
AppName: "block-my-app",
58+
Data: map[string]string{
59+
"ExpectedRejectReason": "app or user text rejected",
60+
},
4761
},
4862
{
4963
AppName: "block-my-app",
5064
Labels: []string{"foo"},
65+
Data: map[string]string{
66+
"ExpectedRejectReason": "app or user text rejected",
67+
},
5168
},
5269
{
5370
AppName: "block-my-app",
5471
Data: map[string]string{
55-
"Version": "42",
72+
"Version": "42",
73+
"ExpectedRejectReason": "app or user text rejected",
5674
},
5775
},
5876
{
5977
AppName: "block-my-app",
6078
Labels: []string{"foo"},
6179
Data: map[string]string{
62-
"Version": "42",
80+
"Version": "42",
81+
"ExpectedRejectReason": "app or user text rejected",
82+
},
83+
},
84+
{
85+
AppName: "my-app",
86+
UserText: "Looks like a recover key abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd",
87+
Data: map[string]string{
88+
"ExpectedRejectReason": "it matches a recovery key and recovery keys are private",
6389
},
6490
},
6591
}
6692
for _, p := range rejectPayloads {
67-
if !cfg.matchesRejectionCondition(&p) {
93+
reject := cfg.matchesRejectionCondition(&p)
94+
if reject != nil {
95+
if *reject != p.Data["ExpectedRejectReason"] {
96+
t.Errorf("payload was rejected with the wrong reason:\n payload=%+v\nconfig=%+v", p, cfg)
97+
}
98+
}
99+
if reject == nil {
68100
t.Errorf("payload was accepted when it should be rejected:\n payload=%+v\nconfig=%+v", p, cfg)
69101
}
70102
}
@@ -112,9 +144,14 @@ func TestConfigRejectionCondition(t *testing.T) {
112144
"Version": "0.1.2",
113145
},
114146
},
147+
{
148+
AppName: "my-app",
149+
UserText: "Some description",
150+
},
115151
}
116152
for _, p := range acceptPayloads {
117-
if cfg.matchesRejectionCondition(&p) {
153+
reject := cfg.matchesRejectionCondition(&p)
154+
if reject != nil {
118155
t.Errorf("payload was rejected when it should be accepted:\n payload=%+v\nconfig=%+v", p, cfg)
119156
}
120157
}

rageshake.sample.yaml

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ listings_auth_pass: secret
1313
allowed_app_names: []
1414

1515
# If any submission matches one of these rejection conditions, the submission is rejected.
16-
# The 'app' field is required, but 'version' and 'label' are both optional. A condition with just
17-
# an 'app' will reject that app altogether, effectively acting as a blocklist for app in contrast to allowed_app_names.
16+
# A condition is made by an union of optional fields: app, version, labels, user text. They all need to match for rejecting the rageshake
17+
# It can also contain an optional reason to explain why this server is rejecting a user's submission.
1818
rejection_conditions:
1919
- app: my-app
2020
version: "0.4.9" # if the submission has a Version which is exactly this value, reject the submission.
@@ -23,6 +23,9 @@ rejection_conditions:
2323
- app: my-app
2424
version: "0.4.9"
2525
label: "nightly" # both label and Version must match for this condition to be true
26+
reason: "this server does not accept rageshakes from nightlies"
27+
- usertext: "(\\w{4}\\s){11}\\w{4}" # reject text containing possible recovery keys
28+
reason: "it matches a recovery key and recovery keys are private"
2629

2730
# a GitHub personal access token (https://github.com/settings/tokens), which
2831
# will be used to create a GitHub issue for each report. It requires

submit.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,15 @@ func (s *submitServer) handleSubmission(w http.ResponseWriter, req *http.Request
226226
http.Error(w, "This server does not accept rageshakes from your application. See https://github.com/matrix-org/rageshake/blob/master/docs/blocked_rageshake.md", 400)
227227
return
228228
}
229-
if s.cfg.matchesRejectionCondition(p) {
230-
log.Printf("Blocking rageshake from app %s because it matches a rejection_condition", p.AppName)
229+
rejection := s.cfg.matchesRejectionCondition(p)
230+
if rejection != nil {
231+
log.Printf("Blocking rageshake from app %s because it matches a rejection_condition: %s", p.AppName, *rejection)
231232
if err := os.RemoveAll(reportDir); err != nil {
232233
log.Printf("Unable to remove report dir %s after rejected upload: %v\n",
233234
reportDir, err)
234235
}
235-
http.Error(w, "This server does not accept rageshakes from your application + version. See https://github.com/matrix-org/rageshake/blob/master/docs/blocked_rageshake.md", 400)
236+
userErrorText := fmt.Sprintf("This server did not accept the rageshake because it matches a rejection condition: %s. See https://github.com/matrix-org/rageshake/blob/master/docs/blocked_rageshake.md", *rejection)
237+
http.Error(w, userErrorText, 400)
236238
return
237239
}
238240

0 commit comments

Comments
 (0)