Repository navigation
Expand file tree
/
Copy pathupload-guard.js
More file actions
135 lines (122 loc) · 4.96 KB
/
Copy pathupload-guard.js
File metadata and controls
135 lines (122 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
Copyright (c) 2026 ClearCode Inc.
*/
'use strict';
import { loadConfig } from './config-loader.js';
import { NetLogger } from './net-logger.js';
import { showDialog } from './dialog.js';
import { message } from './i18n.js';
// The reason codes getBlockReason returns, and what each one is called when it
// has to be read by a person.
const REASON_MESSAGES = {
blockedPath: 'uploadGuardReasonBlockedPath',
blockedExtension: 'uploadGuardReasonBlockedExtension',
pathNotAllowed: 'uploadGuardReasonPathNotAllowed',
extensionNotAllowed: 'uploadGuardReasonExtensionNotAllowed',
};
import { readArray, readBoolean } from './config-value.js';
export const UploadGuard = {
// onBeforeRequest is blocking, so cache the config for synchronous access.
enabled: true,
blockedExtensions: [],
allowedExtensions: [],
allowedPatterns: [],
blockedPatterns: [],
async init () {
const config = await loadConfig();
this.applyConfig(config?.UploadGuard);
},
// Separated from init so that it can be exercised without the browser.
// Members the config leaves out keep their current value.
applyConfig(uploadGuard) {
if (!uploadGuard) return;
this.enabled = readBoolean(uploadGuard, 'Enabled', this.enabled);
this.blockedExtensions =
readArray(uploadGuard, 'BlockedExtensions', this.blockedExtensions);
this.allowedExtensions =
readArray(uploadGuard, 'AllowedExtensions', this.allowedExtensions);
this.allowedPatterns = this.compilePatterns(uploadGuard.AllowedPaths);
this.blockedPatterns = this.compilePatterns(uploadGuard.BlockedPaths);
},
// An unusable pattern is dropped on its own, so one bad entry does not
// silently turn the whole list into "match everything" or "match nothing".
compilePatterns(patterns) {
if (!Array.isArray(patterns)) {
return [];
}
return patterns.map(source => {
try {
// Local paths are case insensitive on Windows.
return new RegExp(source, 'i');
} catch (error) {
console.error('Ignoring an invalid path pattern', source, error?.message);
return null;
}
}).filter(Boolean);
},
hasExtension(file, extensions) {
const lower = file.toLowerCase();
return extensions.some(ext => lower.endsWith(ext.toLowerCase()));
},
// Cancelling a main frame navigation leaves an error page where the form
// was, so that one frame is sent back where it came from instead. Anything
// else is refused outright, so that a script uploading in the background is
// not handed a page of markup as though its upload had succeeded.
buildCancelResponse(isMainFrame) {
if (!isMainFrame) {
return { cancel: true };
}
const html = '<script>history.back();</script>';
return { redirectUrl: `data:text/html;charset=utf-8,${encodeURIComponent(html)}` };
},
// Returns null when the file may be uploaded, otherwise why it may not.
getBlockReason(file) {
if (this.blockedPatterns.some(pattern => pattern.test(file))) {
return 'blockedPath';
}
if (this.hasExtension(file, this.blockedExtensions)) {
return 'blockedExtension';
}
if (this.allowedPatterns.length > 0 &&
!this.allowedPatterns.some(pattern => pattern.test(file))) {
return 'pathNotAllowed';
}
if (this.allowedExtensions.length > 0 &&
!this.hasExtension(file, this.allowedExtensions)) {
return 'extensionNotAllowed';
}
return null;
},
blockedMessage(file, reason) {
return message('uploadGuardBlockedMessage',
[file, message(REASON_MESSAGES[reason] ?? reason)]);
},
onBeforeRequest(details) {
if (!this.enabled) {
return {};
}
if (!details.requestBody?.raw) {
return {};
}
const isMainFrame = (details.type === 'main_frame');
for (const part of details.requestBody.raw) {
if (!part.file) {
continue;
}
const reason = this.getBlockReason(part.file);
if (reason) {
// Not awaited: this listener is blocking and has to answer at
// once. net-logger records nothing unless it is turned on.
NetLogger.record(
'upload-guard', part.file, details.url, details.timeStamp, { reason });
// Not awaited either: the dialog stands until it is dismissed.
showDialog(this.blockedMessage(part.file, reason));
return this.buildCancelResponse(isMainFrame);
}
}
return {};
}
}