@drawbridge/drawbridge-utils 0.0.59 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sanitize.cjs +133 -0
- package/dist/sanitize.d.cts +174 -0
- package/dist/sanitize.d.ts +174 -0
- package/dist/sanitize.js +95 -0
- package/package.json +8 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// lib/sanitize.js
|
|
30
|
+
var sanitize_exports = {};
|
|
31
|
+
__export(sanitize_exports, {
|
|
32
|
+
sanitizeDomain: () => sanitizeDomain,
|
|
33
|
+
sanitizeEmail: () => sanitizeEmail,
|
|
34
|
+
sanitizeQueries: () => sanitizeQueries,
|
|
35
|
+
sanitizeString: () => sanitizeString,
|
|
36
|
+
sanitizeUrl: () => sanitizeUrl
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(sanitize_exports);
|
|
39
|
+
var import_disposable_email_domains = __toESM(require("disposable-email-domains"), 1);
|
|
40
|
+
var SCOPE_KEYS = ["organization", "account", "user", "owner"];
|
|
41
|
+
var DANGEROUS_OPERATORS = ["$where", "$function", "$accumulator"];
|
|
42
|
+
var disposableBlacklist = new Set(import_disposable_email_domains.default.map((d) => d.toLowerCase()));
|
|
43
|
+
var stripDangerousOperators = (value) => {
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value.map(stripDangerousOperators);
|
|
46
|
+
}
|
|
47
|
+
;
|
|
48
|
+
if (value && typeof value === "object") {
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const [key, child] of Object.entries(value)) {
|
|
51
|
+
if (DANGEROUS_OPERATORS.includes(key)) continue;
|
|
52
|
+
result[key] = stripDangerousOperators(child);
|
|
53
|
+
}
|
|
54
|
+
;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
;
|
|
58
|
+
return value;
|
|
59
|
+
};
|
|
60
|
+
var cleanQueryPart = (value) => {
|
|
61
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
62
|
+
const cleaned = stripDangerousOperators(value);
|
|
63
|
+
for (const key of SCOPE_KEYS) {
|
|
64
|
+
delete cleaned[key];
|
|
65
|
+
}
|
|
66
|
+
;
|
|
67
|
+
return cleaned;
|
|
68
|
+
};
|
|
69
|
+
var sanitizeString = (value) => {
|
|
70
|
+
if (value && value !== null) {
|
|
71
|
+
return value.trim();
|
|
72
|
+
} else {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var sanitizeDomain = (value, pattern) => {
|
|
77
|
+
if (!value) return value;
|
|
78
|
+
const cleaned = value.trim().toLowerCase().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
79
|
+
if (pattern) {
|
|
80
|
+
const match = cleaned.match(pattern);
|
|
81
|
+
return match ? match[0] : cleaned;
|
|
82
|
+
}
|
|
83
|
+
return cleaned;
|
|
84
|
+
};
|
|
85
|
+
var sanitizeUrl = (url) => {
|
|
86
|
+
var _a, _b;
|
|
87
|
+
return (_b = (_a = url == null ? void 0 : url.replace(/^http:\/\//, "https://")) == null ? void 0 : _a.replace(/^https:\/\/www\./, "https://")) == null ? void 0 : _b.replace(/\/$/, "");
|
|
88
|
+
};
|
|
89
|
+
var sanitizeEmail = (raw) => {
|
|
90
|
+
if (typeof raw !== "string") return {
|
|
91
|
+
valid: false,
|
|
92
|
+
reason: "format"
|
|
93
|
+
};
|
|
94
|
+
const email = raw.trim().toLowerCase();
|
|
95
|
+
const match = email.match(/^[^\s@]+@([^\s@]+\.[^\s@]+)$/);
|
|
96
|
+
if (!match) return {
|
|
97
|
+
valid: false,
|
|
98
|
+
reason: "format"
|
|
99
|
+
};
|
|
100
|
+
if (disposableBlacklist.has(match[1])) return {
|
|
101
|
+
valid: false,
|
|
102
|
+
reason: "disposable"
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
email,
|
|
106
|
+
valid: true
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
var sanitizeQueries = (req, res, next) => {
|
|
110
|
+
var _a, _b;
|
|
111
|
+
if (req.path.startsWith("/admin/")) {
|
|
112
|
+
next();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
;
|
|
116
|
+
if ((_a = req.query) == null ? void 0 : _a.filters) {
|
|
117
|
+
req.query.filters = cleanQueryPart(req.query.filters);
|
|
118
|
+
}
|
|
119
|
+
;
|
|
120
|
+
if ((_b = req.query) == null ? void 0 : _b.refine) {
|
|
121
|
+
req.query.refine = cleanQueryPart(req.query.refine);
|
|
122
|
+
}
|
|
123
|
+
;
|
|
124
|
+
next();
|
|
125
|
+
};
|
|
126
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
127
|
+
0 && (module.exports = {
|
|
128
|
+
sanitizeDomain,
|
|
129
|
+
sanitizeEmail,
|
|
130
|
+
sanitizeQueries,
|
|
131
|
+
sanitizeString,
|
|
132
|
+
sanitizeUrl
|
|
133
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import disposableDomains from 'disposable-email-domains';
|
|
2
|
+
|
|
3
|
+
const SCOPE_KEYS = [ 'organization', 'account', 'user', 'owner' ];
|
|
4
|
+
const DANGEROUS_OPERATORS = [ '$where', '$function', '$accumulator' ];
|
|
5
|
+
|
|
6
|
+
// Disposable-domain set, built once at module load (~120K domains, ~6MB RSS).
|
|
7
|
+
// O(1) lookup keeps sanitizeEmail cheap. The npm package auto-updates the list —
|
|
8
|
+
// `npm update` periodically refreshes it. (NOTE: importing this module pulls the
|
|
9
|
+
// 6MB set even for consumers that only use sanitizeUrl/sanitizeString; split
|
|
10
|
+
// sanitizeUrl into its own module later if that footprint matters.)
|
|
11
|
+
const disposableBlacklist = new Set( disposableDomains.map( ( d ) => d.toLowerCase() ) );
|
|
12
|
+
|
|
13
|
+
const stripDangerousOperators = ( value ) => {
|
|
14
|
+
|
|
15
|
+
if( Array.isArray( value ) ){
|
|
16
|
+
|
|
17
|
+
return value.map( stripDangerousOperators );
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
if( value && typeof value === 'object' ){
|
|
21
|
+
|
|
22
|
+
const result = {};
|
|
23
|
+
|
|
24
|
+
for( const [ key, child ] of Object.entries( value ) ){
|
|
25
|
+
|
|
26
|
+
if( DANGEROUS_OPERATORS.includes( key ) ) continue;
|
|
27
|
+
|
|
28
|
+
result[ key ] = stripDangerousOperators( child );
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const cleanQueryPart = ( value ) => {
|
|
39
|
+
|
|
40
|
+
if( ! value || typeof value !== 'object' || Array.isArray( value ) ) return value;
|
|
41
|
+
|
|
42
|
+
const cleaned = stripDangerousOperators( value );
|
|
43
|
+
|
|
44
|
+
for( const key of SCOPE_KEYS ){
|
|
45
|
+
|
|
46
|
+
delete cleaned[ key ];
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
return cleaned;
|
|
50
|
+
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sanitizeString = (
|
|
54
|
+
value
|
|
55
|
+
) => {
|
|
56
|
+
|
|
57
|
+
if( value && value !== null ) {
|
|
58
|
+
|
|
59
|
+
//for now will just trim value. Making this a helper in case we end up expanding what gets filtered
|
|
60
|
+
return value.trim();
|
|
61
|
+
|
|
62
|
+
} else {
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const sanitizeDomain = ( value, pattern ) => {
|
|
71
|
+
|
|
72
|
+
if( ! value ) return value;
|
|
73
|
+
|
|
74
|
+
const cleaned = value
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace( /^https?:\/\//i, '' )
|
|
78
|
+
.replace( /\/+$/, '' );
|
|
79
|
+
|
|
80
|
+
if( pattern ){
|
|
81
|
+
|
|
82
|
+
const match = cleaned.match( pattern );
|
|
83
|
+
|
|
84
|
+
return match ? match[ 0 ] : cleaned;
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return cleaned;
|
|
89
|
+
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sanitizeUrl = ( url ) => url
|
|
93
|
+
?.replace( /^http:\/\//, 'https://' )
|
|
94
|
+
?.replace( /^https:\/\/www\./, 'https://' )
|
|
95
|
+
?.replace( /\/$/, '' );
|
|
96
|
+
|
|
97
|
+
// Normalize + validate an email address. Trim, lowercase, basic shape
|
|
98
|
+
// check, and reject disposable-domain providers. Returns the normalized
|
|
99
|
+
// email on success; never throws.
|
|
100
|
+
//
|
|
101
|
+
// { valid: true, email: 'darren@cursor.io' }
|
|
102
|
+
// { valid: false, reason: 'format' }
|
|
103
|
+
// { valid: false, reason: 'disposable' }
|
|
104
|
+
const sanitizeEmail = ( raw ) => {
|
|
105
|
+
|
|
106
|
+
if( typeof raw !== 'string' ) return {
|
|
107
|
+
valid : false,
|
|
108
|
+
reason : 'format'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const email = raw.trim().toLowerCase();
|
|
112
|
+
|
|
113
|
+
// Minimal RFC-ish shape — defers full validation to the email provider
|
|
114
|
+
// on delivery. We just need to reject obvious garbage and extract a
|
|
115
|
+
// domain for the disposable-list check.
|
|
116
|
+
const match = email.match( /^[^\s@]+@([^\s@]+\.[^\s@]+)$/ );
|
|
117
|
+
|
|
118
|
+
if( ! match ) return {
|
|
119
|
+
valid : false,
|
|
120
|
+
reason : 'format'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if( disposableBlacklist.has( match[ 1 ] ) ) return {
|
|
124
|
+
valid : false,
|
|
125
|
+
reason : 'disposable'
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
email,
|
|
130
|
+
valid : true
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Express middleware. Strips two specific attack surfaces from `req.query.filters`
|
|
136
|
+
// and `req.query.refine` before they flow into Mongo queries via the
|
|
137
|
+
// `...filters` / `...refine` spread pattern:
|
|
138
|
+
//
|
|
139
|
+
// 1. Scope-key override at the top level — keys that name a tenant scope
|
|
140
|
+
// (organization, account, user, owner). Without this guard, an attacker can
|
|
141
|
+
// pass `?filters[organization]=OTHER_ORG_ID` or
|
|
142
|
+
// `?filters[organization][$ne]=null` and override the route's scope.
|
|
143
|
+
//
|
|
144
|
+
// 2. JS-execution operators ($where, $function, $accumulator) anywhere in the
|
|
145
|
+
// tree — these run arbitrary server-side JS in Mongo. No client UI builds them.
|
|
146
|
+
//
|
|
147
|
+
// $or / $and / $in / $ne / $regex / etc. on non-scope fields are intentionally
|
|
148
|
+
// allowed — the dashboard's filter builder (components/form-filters.js) produces
|
|
149
|
+
// them and they cannot bypass an outer-scope AND clause.
|
|
150
|
+
//
|
|
151
|
+
// Admin paths (`/admin/...`) bypass entirely.
|
|
152
|
+
const sanitizeQueries = ( req, res, next ) => {
|
|
153
|
+
|
|
154
|
+
if( req.path.startsWith( '/admin/' ) ){
|
|
155
|
+
|
|
156
|
+
next();
|
|
157
|
+
return;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
if( req.query?.filters ){
|
|
161
|
+
|
|
162
|
+
req.query.filters = cleanQueryPart( req.query.filters );
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
if( req.query?.refine ){
|
|
166
|
+
|
|
167
|
+
req.query.refine = cleanQueryPart( req.query.refine );
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
next();
|
|
171
|
+
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export { sanitizeDomain, sanitizeEmail, sanitizeQueries, sanitizeString, sanitizeUrl };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import disposableDomains from 'disposable-email-domains';
|
|
2
|
+
|
|
3
|
+
const SCOPE_KEYS = [ 'organization', 'account', 'user', 'owner' ];
|
|
4
|
+
const DANGEROUS_OPERATORS = [ '$where', '$function', '$accumulator' ];
|
|
5
|
+
|
|
6
|
+
// Disposable-domain set, built once at module load (~120K domains, ~6MB RSS).
|
|
7
|
+
// O(1) lookup keeps sanitizeEmail cheap. The npm package auto-updates the list —
|
|
8
|
+
// `npm update` periodically refreshes it. (NOTE: importing this module pulls the
|
|
9
|
+
// 6MB set even for consumers that only use sanitizeUrl/sanitizeString; split
|
|
10
|
+
// sanitizeUrl into its own module later if that footprint matters.)
|
|
11
|
+
const disposableBlacklist = new Set( disposableDomains.map( ( d ) => d.toLowerCase() ) );
|
|
12
|
+
|
|
13
|
+
const stripDangerousOperators = ( value ) => {
|
|
14
|
+
|
|
15
|
+
if( Array.isArray( value ) ){
|
|
16
|
+
|
|
17
|
+
return value.map( stripDangerousOperators );
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
if( value && typeof value === 'object' ){
|
|
21
|
+
|
|
22
|
+
const result = {};
|
|
23
|
+
|
|
24
|
+
for( const [ key, child ] of Object.entries( value ) ){
|
|
25
|
+
|
|
26
|
+
if( DANGEROUS_OPERATORS.includes( key ) ) continue;
|
|
27
|
+
|
|
28
|
+
result[ key ] = stripDangerousOperators( child );
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const cleanQueryPart = ( value ) => {
|
|
39
|
+
|
|
40
|
+
if( ! value || typeof value !== 'object' || Array.isArray( value ) ) return value;
|
|
41
|
+
|
|
42
|
+
const cleaned = stripDangerousOperators( value );
|
|
43
|
+
|
|
44
|
+
for( const key of SCOPE_KEYS ){
|
|
45
|
+
|
|
46
|
+
delete cleaned[ key ];
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
return cleaned;
|
|
50
|
+
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sanitizeString = (
|
|
54
|
+
value
|
|
55
|
+
) => {
|
|
56
|
+
|
|
57
|
+
if( value && value !== null ) {
|
|
58
|
+
|
|
59
|
+
//for now will just trim value. Making this a helper in case we end up expanding what gets filtered
|
|
60
|
+
return value.trim();
|
|
61
|
+
|
|
62
|
+
} else {
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const sanitizeDomain = ( value, pattern ) => {
|
|
71
|
+
|
|
72
|
+
if( ! value ) return value;
|
|
73
|
+
|
|
74
|
+
const cleaned = value
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace( /^https?:\/\//i, '' )
|
|
78
|
+
.replace( /\/+$/, '' );
|
|
79
|
+
|
|
80
|
+
if( pattern ){
|
|
81
|
+
|
|
82
|
+
const match = cleaned.match( pattern );
|
|
83
|
+
|
|
84
|
+
return match ? match[ 0 ] : cleaned;
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return cleaned;
|
|
89
|
+
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sanitizeUrl = ( url ) => url
|
|
93
|
+
?.replace( /^http:\/\//, 'https://' )
|
|
94
|
+
?.replace( /^https:\/\/www\./, 'https://' )
|
|
95
|
+
?.replace( /\/$/, '' );
|
|
96
|
+
|
|
97
|
+
// Normalize + validate an email address. Trim, lowercase, basic shape
|
|
98
|
+
// check, and reject disposable-domain providers. Returns the normalized
|
|
99
|
+
// email on success; never throws.
|
|
100
|
+
//
|
|
101
|
+
// { valid: true, email: 'darren@cursor.io' }
|
|
102
|
+
// { valid: false, reason: 'format' }
|
|
103
|
+
// { valid: false, reason: 'disposable' }
|
|
104
|
+
const sanitizeEmail = ( raw ) => {
|
|
105
|
+
|
|
106
|
+
if( typeof raw !== 'string' ) return {
|
|
107
|
+
valid : false,
|
|
108
|
+
reason : 'format'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const email = raw.trim().toLowerCase();
|
|
112
|
+
|
|
113
|
+
// Minimal RFC-ish shape — defers full validation to the email provider
|
|
114
|
+
// on delivery. We just need to reject obvious garbage and extract a
|
|
115
|
+
// domain for the disposable-list check.
|
|
116
|
+
const match = email.match( /^[^\s@]+@([^\s@]+\.[^\s@]+)$/ );
|
|
117
|
+
|
|
118
|
+
if( ! match ) return {
|
|
119
|
+
valid : false,
|
|
120
|
+
reason : 'format'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if( disposableBlacklist.has( match[ 1 ] ) ) return {
|
|
124
|
+
valid : false,
|
|
125
|
+
reason : 'disposable'
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
email,
|
|
130
|
+
valid : true
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Express middleware. Strips two specific attack surfaces from `req.query.filters`
|
|
136
|
+
// and `req.query.refine` before they flow into Mongo queries via the
|
|
137
|
+
// `...filters` / `...refine` spread pattern:
|
|
138
|
+
//
|
|
139
|
+
// 1. Scope-key override at the top level — keys that name a tenant scope
|
|
140
|
+
// (organization, account, user, owner). Without this guard, an attacker can
|
|
141
|
+
// pass `?filters[organization]=OTHER_ORG_ID` or
|
|
142
|
+
// `?filters[organization][$ne]=null` and override the route's scope.
|
|
143
|
+
//
|
|
144
|
+
// 2. JS-execution operators ($where, $function, $accumulator) anywhere in the
|
|
145
|
+
// tree — these run arbitrary server-side JS in Mongo. No client UI builds them.
|
|
146
|
+
//
|
|
147
|
+
// $or / $and / $in / $ne / $regex / etc. on non-scope fields are intentionally
|
|
148
|
+
// allowed — the dashboard's filter builder (components/form-filters.js) produces
|
|
149
|
+
// them and they cannot bypass an outer-scope AND clause.
|
|
150
|
+
//
|
|
151
|
+
// Admin paths (`/admin/...`) bypass entirely.
|
|
152
|
+
const sanitizeQueries = ( req, res, next ) => {
|
|
153
|
+
|
|
154
|
+
if( req.path.startsWith( '/admin/' ) ){
|
|
155
|
+
|
|
156
|
+
next();
|
|
157
|
+
return;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
if( req.query?.filters ){
|
|
161
|
+
|
|
162
|
+
req.query.filters = cleanQueryPart( req.query.filters );
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
if( req.query?.refine ){
|
|
166
|
+
|
|
167
|
+
req.query.refine = cleanQueryPart( req.query.refine );
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
next();
|
|
171
|
+
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export { sanitizeDomain, sanitizeEmail, sanitizeQueries, sanitizeString, sanitizeUrl };
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// lib/sanitize.js
|
|
2
|
+
import disposableDomains from "disposable-email-domains";
|
|
3
|
+
var SCOPE_KEYS = ["organization", "account", "user", "owner"];
|
|
4
|
+
var DANGEROUS_OPERATORS = ["$where", "$function", "$accumulator"];
|
|
5
|
+
var disposableBlacklist = new Set(disposableDomains.map((d) => d.toLowerCase()));
|
|
6
|
+
var stripDangerousOperators = (value) => {
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return value.map(stripDangerousOperators);
|
|
9
|
+
}
|
|
10
|
+
;
|
|
11
|
+
if (value && typeof value === "object") {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [key, child] of Object.entries(value)) {
|
|
14
|
+
if (DANGEROUS_OPERATORS.includes(key)) continue;
|
|
15
|
+
result[key] = stripDangerousOperators(child);
|
|
16
|
+
}
|
|
17
|
+
;
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
;
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var cleanQueryPart = (value) => {
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
25
|
+
const cleaned = stripDangerousOperators(value);
|
|
26
|
+
for (const key of SCOPE_KEYS) {
|
|
27
|
+
delete cleaned[key];
|
|
28
|
+
}
|
|
29
|
+
;
|
|
30
|
+
return cleaned;
|
|
31
|
+
};
|
|
32
|
+
var sanitizeString = (value) => {
|
|
33
|
+
if (value && value !== null) {
|
|
34
|
+
return value.trim();
|
|
35
|
+
} else {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var sanitizeDomain = (value, pattern) => {
|
|
40
|
+
if (!value) return value;
|
|
41
|
+
const cleaned = value.trim().toLowerCase().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
42
|
+
if (pattern) {
|
|
43
|
+
const match = cleaned.match(pattern);
|
|
44
|
+
return match ? match[0] : cleaned;
|
|
45
|
+
}
|
|
46
|
+
return cleaned;
|
|
47
|
+
};
|
|
48
|
+
var sanitizeUrl = (url) => {
|
|
49
|
+
var _a, _b;
|
|
50
|
+
return (_b = (_a = url == null ? void 0 : url.replace(/^http:\/\//, "https://")) == null ? void 0 : _a.replace(/^https:\/\/www\./, "https://")) == null ? void 0 : _b.replace(/\/$/, "");
|
|
51
|
+
};
|
|
52
|
+
var sanitizeEmail = (raw) => {
|
|
53
|
+
if (typeof raw !== "string") return {
|
|
54
|
+
valid: false,
|
|
55
|
+
reason: "format"
|
|
56
|
+
};
|
|
57
|
+
const email = raw.trim().toLowerCase();
|
|
58
|
+
const match = email.match(/^[^\s@]+@([^\s@]+\.[^\s@]+)$/);
|
|
59
|
+
if (!match) return {
|
|
60
|
+
valid: false,
|
|
61
|
+
reason: "format"
|
|
62
|
+
};
|
|
63
|
+
if (disposableBlacklist.has(match[1])) return {
|
|
64
|
+
valid: false,
|
|
65
|
+
reason: "disposable"
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
email,
|
|
69
|
+
valid: true
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
var sanitizeQueries = (req, res, next) => {
|
|
73
|
+
var _a, _b;
|
|
74
|
+
if (req.path.startsWith("/admin/")) {
|
|
75
|
+
next();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
;
|
|
79
|
+
if ((_a = req.query) == null ? void 0 : _a.filters) {
|
|
80
|
+
req.query.filters = cleanQueryPart(req.query.filters);
|
|
81
|
+
}
|
|
82
|
+
;
|
|
83
|
+
if ((_b = req.query) == null ? void 0 : _b.refine) {
|
|
84
|
+
req.query.refine = cleanQueryPart(req.query.refine);
|
|
85
|
+
}
|
|
86
|
+
;
|
|
87
|
+
next();
|
|
88
|
+
};
|
|
89
|
+
export {
|
|
90
|
+
sanitizeDomain,
|
|
91
|
+
sanitizeEmail,
|
|
92
|
+
sanitizeQueries,
|
|
93
|
+
sanitizeString,
|
|
94
|
+
sanitizeUrl
|
|
95
|
+
};
|
package/package.json
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"@drawbridge/drawbridge-agents": "0.0.10",
|
|
5
|
-
"@drawbridge/drawbridge-telemetry": "0.0.
|
|
5
|
+
"@drawbridge/drawbridge-telemetry": "0.0.14",
|
|
6
6
|
"@google/genai": "1.30.0",
|
|
7
7
|
"axios": "1.16.0",
|
|
8
8
|
"currency-codes": "2.2.0",
|
|
9
|
+
"disposable-email-domains": "1.0.62",
|
|
9
10
|
"nanoid": "3.3.8",
|
|
10
11
|
"qs": "6.15.2",
|
|
11
12
|
"slugify": "1.6.6",
|
|
@@ -93,6 +94,11 @@
|
|
|
93
94
|
"import": "./dist/redirect.js",
|
|
94
95
|
"require": "./dist/redirect.cjs"
|
|
95
96
|
},
|
|
97
|
+
"./sanitize": {
|
|
98
|
+
"types": "./dist/sanitize.d.ts",
|
|
99
|
+
"import": "./dist/sanitize.js",
|
|
100
|
+
"require": "./dist/sanitize.cjs"
|
|
101
|
+
},
|
|
96
102
|
"./cdn": {
|
|
97
103
|
"types": "./dist/cdn.d.ts",
|
|
98
104
|
"import": "./dist/cdn.js",
|
|
@@ -154,5 +160,5 @@
|
|
|
154
160
|
"build": "tsup && npm publish"
|
|
155
161
|
},
|
|
156
162
|
"types": "dist/index.d.ts",
|
|
157
|
-
"version": "0.0.
|
|
163
|
+
"version": "0.0.60"
|
|
158
164
|
}
|