@forge/csp 0.0.0-experimental-d18f8dd
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/CHANGELOG.md +247 -0
- package/out/csp/csp-injection-service.d.ts +10 -0
- package/out/csp/csp-injection-service.d.ts.map +1 -0
- package/out/csp/csp-injection-service.js +82 -0
- package/out/csp/csp-processing-service.d.ts +21 -0
- package/out/csp/csp-processing-service.d.ts.map +1 -0
- package/out/csp/csp-processing-service.js +117 -0
- package/out/csp/index.d.ts +3 -0
- package/out/csp/index.d.ts.map +1 -0
- package/out/csp/index.js +5 -0
- package/out/egress/egress-filtering-service.d.ts +11 -0
- package/out/egress/egress-filtering-service.d.ts.map +1 -0
- package/out/egress/egress-filtering-service.js +46 -0
- package/out/egress/index.d.ts +3 -0
- package/out/egress/index.d.ts.map +1 -0
- package/out/egress/index.js +5 -0
- package/out/egress/utils.d.ts +3 -0
- package/out/egress/utils.d.ts.map +1 -0
- package/out/egress/utils.js +31 -0
- package/out/index.d.ts +4 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +6 -0
- package/out/types.d.ts +16 -0
- package/out/types.d.ts.map +1 -0
- package/out/types.js +13 -0
- package/package.json +23 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# @forge/csp
|
|
2
|
+
|
|
3
|
+
## 0.0.0-experimental-d18f8dd
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d18f8dd: Force bump
|
|
8
|
+
|
|
9
|
+
## 1.7.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 4b41a80: Added egress messaging to install prompts
|
|
14
|
+
|
|
15
|
+
## 1.7.1-next.0
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 4b41a80: Added egress messaging to install prompts
|
|
20
|
+
|
|
21
|
+
## 1.7.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- ef00257: Add \*.jira.com to allowed host site list
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- d7a1fe3: Update dependencies to remove any transitive dependencies on request
|
|
30
|
+
|
|
31
|
+
## 1.7.0-next.1
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- d7a1fe3: Update dependencies to remove any transitive dependencies on request
|
|
36
|
+
|
|
37
|
+
## 1.7.0-next.0
|
|
38
|
+
|
|
39
|
+
### Minor Changes
|
|
40
|
+
|
|
41
|
+
- ef00257: Add \*.jira.com to allowed host site list
|
|
42
|
+
|
|
43
|
+
## 1.6.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- 8714f5a: Add support for fonts and frames as part of Egress Permissions for Custom UI apps
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- f8ae8a2: Add support for Bitbucket origin in Custom UI
|
|
52
|
+
|
|
53
|
+
## 1.6.0-next.1
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- f8ae8a2: Add support for Bitbucket origin in Custom UI
|
|
58
|
+
|
|
59
|
+
## 1.6.0-next.0
|
|
60
|
+
|
|
61
|
+
### Minor Changes
|
|
62
|
+
|
|
63
|
+
- 8714f5a: Add support for fonts and frames as part of Egress Permissions for Custom UI apps
|
|
64
|
+
|
|
65
|
+
## 1.5.0
|
|
66
|
+
|
|
67
|
+
### Minor Changes
|
|
68
|
+
|
|
69
|
+
- 638194f: Fix logic to detect missing fetch egress permission
|
|
70
|
+
|
|
71
|
+
## 1.5.0-next.0
|
|
72
|
+
|
|
73
|
+
### Minor Changes
|
|
74
|
+
|
|
75
|
+
- 638194f: Fix logic to detect missing fetch egress permission
|
|
76
|
+
|
|
77
|
+
## 1.4.0
|
|
78
|
+
|
|
79
|
+
### Minor Changes
|
|
80
|
+
|
|
81
|
+
- 05f608f: Added external fetch linting
|
|
82
|
+
|
|
83
|
+
### Patch Changes
|
|
84
|
+
|
|
85
|
+
- bd9194a: Added error protection to egress filtering for URLs with no protocol
|
|
86
|
+
|
|
87
|
+
## 1.4.0-next.1
|
|
88
|
+
|
|
89
|
+
### Patch Changes
|
|
90
|
+
|
|
91
|
+
- bd9194a: Added error protection to egress filtering for URLs with no protocol
|
|
92
|
+
|
|
93
|
+
## 1.4.0-next.0
|
|
94
|
+
|
|
95
|
+
### Minor Changes
|
|
96
|
+
|
|
97
|
+
- 05f608f: Added external fetch linting
|
|
98
|
+
|
|
99
|
+
## 1.3.0
|
|
100
|
+
|
|
101
|
+
### Minor Changes
|
|
102
|
+
|
|
103
|
+
- 9ec2911: Allow style-src as part of Egress Permissions for Custom UI apps
|
|
104
|
+
|
|
105
|
+
### Patch Changes
|
|
106
|
+
|
|
107
|
+
- 2ddcdb2: Update frame-ancestors for dev
|
|
108
|
+
- 2b3c55d: Fix to restrict frame ancestors of Custom UI apps
|
|
109
|
+
|
|
110
|
+
## 1.3.0-next.2
|
|
111
|
+
|
|
112
|
+
### Patch Changes
|
|
113
|
+
|
|
114
|
+
- 2ddcdb2: Update frame-ancestors for dev
|
|
115
|
+
|
|
116
|
+
## 1.3.0-next.1
|
|
117
|
+
|
|
118
|
+
### Minor Changes
|
|
119
|
+
|
|
120
|
+
- 9ec2911: Allow style-src as part of Egress Permissions for Custom UI apps
|
|
121
|
+
|
|
122
|
+
## 1.2.1-next.0
|
|
123
|
+
|
|
124
|
+
### Patch Changes
|
|
125
|
+
|
|
126
|
+
- 2b3c55d: Fix to restrict frame ancestors of Custom UI apps
|
|
127
|
+
|
|
128
|
+
## 1.2.0
|
|
129
|
+
|
|
130
|
+
### Minor Changes
|
|
131
|
+
|
|
132
|
+
- 6c482ef: Add `allow-downloads allow-modals` to sandbox
|
|
133
|
+
|
|
134
|
+
## 1.2.0-next.0
|
|
135
|
+
|
|
136
|
+
### Minor Changes
|
|
137
|
+
|
|
138
|
+
- 6c482ef: Add `allow-downloads allow-modals` to sandbox
|
|
139
|
+
|
|
140
|
+
## 1.1.0
|
|
141
|
+
|
|
142
|
+
### Minor Changes
|
|
143
|
+
|
|
144
|
+
- f478087: Added logic handle external egress permissions
|
|
145
|
+
- c3ee9e7: Convert permissions.external to CSP options for Custom UI
|
|
146
|
+
|
|
147
|
+
### Patch Changes
|
|
148
|
+
|
|
149
|
+
- 74a0279: Allowlist images from Atlassian API inside Custom UI apps
|
|
150
|
+
- f8bb329: Adding on user defined CSP from the manifest
|
|
151
|
+
|
|
152
|
+
## 1.1.0-next.3
|
|
153
|
+
|
|
154
|
+
### Minor Changes
|
|
155
|
+
|
|
156
|
+
- c3ee9e7: Convert permissions.external to CSP options for Custom UI
|
|
157
|
+
|
|
158
|
+
## 1.1.0-next.2
|
|
159
|
+
|
|
160
|
+
### Minor Changes
|
|
161
|
+
|
|
162
|
+
- f478087: Added logic handle external egress permissions
|
|
163
|
+
|
|
164
|
+
## 1.0.2-next.1
|
|
165
|
+
|
|
166
|
+
### Patch Changes
|
|
167
|
+
|
|
168
|
+
- f8bb329: Adding on user defined CSP from the manifest
|
|
169
|
+
|
|
170
|
+
## 1.0.2-next.0
|
|
171
|
+
|
|
172
|
+
### Patch Changes
|
|
173
|
+
|
|
174
|
+
- 8ad9442: Allowlist images from Atlassian API inside Custom UI apps
|
|
175
|
+
|
|
176
|
+
## 1.0.1
|
|
177
|
+
|
|
178
|
+
### Patch Changes
|
|
179
|
+
|
|
180
|
+
- 4ef25ff: change occurrences of Csp to CSP for consistency
|
|
181
|
+
|
|
182
|
+
## 1.0.1-next.0
|
|
183
|
+
|
|
184
|
+
### Patch Changes
|
|
185
|
+
|
|
186
|
+
- 4ef25ff: change occurrences of Csp to CSP for consistency
|
|
187
|
+
|
|
188
|
+
## 1.0.0
|
|
189
|
+
|
|
190
|
+
### Major Changes
|
|
191
|
+
|
|
192
|
+
- 1daf2c5: Forge packages to 1.0.0 for upcoming platform GA 🎉
|
|
193
|
+
|
|
194
|
+
### Patch Changes
|
|
195
|
+
|
|
196
|
+
- ca7a9e1: Add local tunnel Custom UI CSP reported server
|
|
197
|
+
|
|
198
|
+
## 1.0.0-next.1
|
|
199
|
+
|
|
200
|
+
### Major Changes
|
|
201
|
+
|
|
202
|
+
- 1daf2c5: Forge is now generally available 🎉
|
|
203
|
+
|
|
204
|
+
## 0.1.2-next.0
|
|
205
|
+
|
|
206
|
+
### Patch Changes
|
|
207
|
+
|
|
208
|
+
- 1b3bfe1: Add local tunnel Custom UI CSP reported server
|
|
209
|
+
|
|
210
|
+
## 0.1.1
|
|
211
|
+
|
|
212
|
+
### Patch Changes
|
|
213
|
+
|
|
214
|
+
- 69064a4: Add secure.gravatar.com to img-src
|
|
215
|
+
|
|
216
|
+
## 0.1.1-next.0
|
|
217
|
+
|
|
218
|
+
### Patch Changes
|
|
219
|
+
|
|
220
|
+
- 69064a4: Add secure.gravatar.com to img-src
|
|
221
|
+
|
|
222
|
+
## 0.1.0
|
|
223
|
+
|
|
224
|
+
### Minor Changes
|
|
225
|
+
|
|
226
|
+
- 41dcd69: Moved CSP logic to its own package and bridge script to cli-shared
|
|
227
|
+
|
|
228
|
+
### Patch Changes
|
|
229
|
+
|
|
230
|
+
- a7f8517: Move cli-shared out of the main deps of the CDN
|
|
231
|
+
|
|
232
|
+
## 0.1.0-next.1
|
|
233
|
+
|
|
234
|
+
### Patch Changes
|
|
235
|
+
|
|
236
|
+
- a7f8517: Move cli-shared out of the main deps of the CDN
|
|
237
|
+
|
|
238
|
+
## 0.1.0-next.0
|
|
239
|
+
|
|
240
|
+
### Minor Changes
|
|
241
|
+
|
|
242
|
+
- 41dcd69: Moved CSP logic to its own package and bridge script to cli-shared
|
|
243
|
+
|
|
244
|
+
### Patch Changes
|
|
245
|
+
|
|
246
|
+
- Updated dependencies [41dcd69]
|
|
247
|
+
- @forge/cli-shared@0.13.0-next.5
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LambdaEnvironment } from '@forge/cli-shared';
|
|
2
|
+
import { CSPDetails } from '../types';
|
|
3
|
+
export declare class CSPInjectionService {
|
|
4
|
+
private getCSPReportUri;
|
|
5
|
+
private getForgeGlobalCSP;
|
|
6
|
+
private getExistingCSPDetails;
|
|
7
|
+
private getFrameAncestors;
|
|
8
|
+
getInjectableCSP: (existingCSPDetails: CSPDetails, env: LambdaEnvironment, tunnelCSPReporterUri?: string | undefined) => string[];
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=csp-injection-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp-injection-service.d.ts","sourceRoot":"","sources":["../../src/csp/csp-injection-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,UAAU,EAAmB,MAAM,UAAU,CAAC;AAUvD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,iBAAiB;IAYlB,gBAAgB,uBACD,UAAU,OACzB,iBAAiB,gDAErB,MAAM,EAAE,CA4CT;CACH"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CSPInjectionService = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const atlassianImageHosts = {
|
|
6
|
+
dev: ['https://avatar-management--avatars.us-west-2.staging.public.atl-paas.net', 'https://api.dev.atlassian.com'],
|
|
7
|
+
stg: ['https://avatar-management--avatars.us-west-2.staging.public.atl-paas.net', 'https://api.stg.atlassian.com'],
|
|
8
|
+
prod: ['https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net', 'https://api.atlassian.com']
|
|
9
|
+
};
|
|
10
|
+
const gravatarUrl = 'https://secure.gravatar.com';
|
|
11
|
+
class CSPInjectionService {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.getInjectableCSP = (existingCSPDetails, env, tunnelCSPReporterUri) => {
|
|
14
|
+
const reportUri = tunnelCSPReporterUri || this.getCSPReportUri(env);
|
|
15
|
+
const defaultSrc = `'self'`;
|
|
16
|
+
const frameAncestors = ["'self'", ...this.getFrameAncestors(env)].join(' ');
|
|
17
|
+
const frameSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FRAME_SRC, existingCSPDetails)].join(' ');
|
|
18
|
+
const fontSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FONT_SRC, existingCSPDetails)].join(' ');
|
|
19
|
+
const imgSrc = [
|
|
20
|
+
"'self'",
|
|
21
|
+
'data:',
|
|
22
|
+
'blob:',
|
|
23
|
+
gravatarUrl,
|
|
24
|
+
...atlassianImageHosts[env],
|
|
25
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.IMG_SRC, existingCSPDetails)
|
|
26
|
+
].join(' ');
|
|
27
|
+
const mediaSrc = [
|
|
28
|
+
"'self'",
|
|
29
|
+
'data:',
|
|
30
|
+
'blob:',
|
|
31
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.MEDIA_SRC, existingCSPDetails)
|
|
32
|
+
].join(' ');
|
|
33
|
+
const connectSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.CONNECT_SRC, existingCSPDetails)].join(' ');
|
|
34
|
+
const scriptSrc = [
|
|
35
|
+
"'self'",
|
|
36
|
+
this.getForgeGlobalCSP(env),
|
|
37
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.SCRIPT_SRC, existingCSPDetails)
|
|
38
|
+
].join(' ');
|
|
39
|
+
const styleSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.STYLE_SRC, existingCSPDetails)].join(' ');
|
|
40
|
+
return [
|
|
41
|
+
`default-src ${defaultSrc}`,
|
|
42
|
+
`frame-ancestors ${frameAncestors}`,
|
|
43
|
+
`frame-src ${frameSrc}`,
|
|
44
|
+
`font-src ${fontSrc}`,
|
|
45
|
+
`img-src ${imgSrc}`,
|
|
46
|
+
`media-src ${mediaSrc}`,
|
|
47
|
+
`connect-src ${connectSrc}`,
|
|
48
|
+
`script-src ${scriptSrc}`,
|
|
49
|
+
`style-src ${styleSrc}`,
|
|
50
|
+
`form-action 'self'`,
|
|
51
|
+
`sandbox allow-downloads allow-forms allow-modals allow-same-origin allow-scripts`,
|
|
52
|
+
`report-uri ${reportUri}`
|
|
53
|
+
];
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
getCSPReportUri(env, tunnelCSPReporterUri) {
|
|
57
|
+
if (tunnelCSPReporterUri)
|
|
58
|
+
return tunnelCSPReporterUri;
|
|
59
|
+
if (env === 'prod')
|
|
60
|
+
return 'https://web-security-reports.services.atlassian.com/csp-report/forge-cdn';
|
|
61
|
+
return 'https://web-security-reports.stg.services.atlassian.com/csp-report/forge-cdn';
|
|
62
|
+
}
|
|
63
|
+
getForgeGlobalCSP(env) {
|
|
64
|
+
return `https://forge.cdn.${env}.atlassian-dev.net`;
|
|
65
|
+
}
|
|
66
|
+
getExistingCSPDetails(cspType, cspDetails) {
|
|
67
|
+
var _a;
|
|
68
|
+
return (_a = cspDetails[cspType]) !== null && _a !== void 0 ? _a : [];
|
|
69
|
+
}
|
|
70
|
+
getFrameAncestors(env) {
|
|
71
|
+
if (env === 'prod')
|
|
72
|
+
return ['*.atlassian.net', 'bitbucket.org', '*.jira.com'];
|
|
73
|
+
return [
|
|
74
|
+
'*.jira-dev.com',
|
|
75
|
+
'http://localhost:*',
|
|
76
|
+
'*.devbucket.org',
|
|
77
|
+
'https://staging.bb-inf.net',
|
|
78
|
+
'https://integration.bb-inf.net'
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.CSPInjectionService = CSPInjectionService;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Logger } from '@forge/cli-shared';
|
|
2
|
+
import type { Permissions } from '@forge/manifest';
|
|
3
|
+
import { ContentPermissions, CSPDetails, DocumentBody } from '../types';
|
|
4
|
+
export declare class CSPProcessingService {
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(logger: Pick<Logger, 'info'>);
|
|
7
|
+
getCspDetails(body: DocumentBody, permissions: Permissions): CSPDetails;
|
|
8
|
+
getInvalidCspPermissions(contentPermissions: ContentPermissions): string[];
|
|
9
|
+
private mapExternalPermissionsToCsp;
|
|
10
|
+
private getStyleSrc;
|
|
11
|
+
private getScriptSrc;
|
|
12
|
+
private extractUniqueHashes;
|
|
13
|
+
private getInlineScriptHashes;
|
|
14
|
+
private hashScript;
|
|
15
|
+
private isValidUserScriptSrc;
|
|
16
|
+
private isValidUserStyleSrc;
|
|
17
|
+
private isSafeCsp;
|
|
18
|
+
private isValidHash;
|
|
19
|
+
private getDeprecatedUserCsp;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=csp-processing-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp-processing-service.d.ts","sourceRoot":"","sources":["../../src/csp/csp-processing-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,YAAY,EAAuB,MAAM,UAAU,CAAC;AAiB7F,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;IAElD,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,GAAG,UAAU;IAkBvE,wBAAwB,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,MAAM,EAAE;IASjF,OAAO,CAAC,2BAA2B;IAcnC,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,mBAAmB;IAoB3B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,oBAAoB;CAa7B"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CSPProcessingService = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const cheerio_1 = tslib_1.__importDefault(require("cheerio"));
|
|
6
|
+
const content_security_policy_parser_1 = tslib_1.__importDefault(require("content-security-policy-parser"));
|
|
7
|
+
const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
|
8
|
+
const STYLE_SRC_ALLOWLIST = [`'unsafe-inline'`];
|
|
9
|
+
const SCRIPT_SRC_ALLOWLIST = [`'unsafe-inline'`, `'unsafe-eval'`, `'unsafe-hashes'`];
|
|
10
|
+
const BASE_64_HASH_PATTERNS = [
|
|
11
|
+
/^'sha256-[a-zA-Z0-9=+/]{44}'$/,
|
|
12
|
+
/^'sha384-[a-zA-Z0-9=+/]{64}'$/,
|
|
13
|
+
/^'sha512-[a-zA-Z0-9=+/]{88}'$/
|
|
14
|
+
];
|
|
15
|
+
class CSPProcessingService {
|
|
16
|
+
constructor(logger) {
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
}
|
|
19
|
+
getCspDetails(body, permissions) {
|
|
20
|
+
var _a, _b;
|
|
21
|
+
const { scripts, styles } = (_a = permissions === null || permissions === void 0 ? void 0 : permissions.content) !== null && _a !== void 0 ? _a : { scripts: [], styles: [] };
|
|
22
|
+
const external = (_b = permissions === null || permissions === void 0 ? void 0 : permissions.external) !== null && _b !== void 0 ? _b : {};
|
|
23
|
+
const $ = cheerio_1.default.load(body);
|
|
24
|
+
const _c = this.mapExternalPermissionsToCsp(external), { 'script-src': scriptSrc, 'style-src': styleSrc } = _c, mappedExternalCsp = tslib_1.__rest(_c, ['script-src', 'style-src']);
|
|
25
|
+
return Object.assign({ 'style-src': [...this.getStyleSrc($, styles), ...styleSrc], 'script-src': [...this.getScriptSrc($, scripts), ...scriptSrc] }, mappedExternalCsp);
|
|
26
|
+
}
|
|
27
|
+
getInvalidCspPermissions(contentPermissions) {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
const { styles, scripts } = contentPermissions;
|
|
30
|
+
const invalidStyles = (_a = styles === null || styles === void 0 ? void 0 : styles.filter((styleSrc) => !this.isValidUserStyleSrc(`'${styleSrc}'`))) !== null && _a !== void 0 ? _a : [];
|
|
31
|
+
const invalidScripts = (_b = scripts === null || scripts === void 0 ? void 0 : scripts.filter((scriptSrc) => !this.isValidUserScriptSrc(`'${scriptSrc}'`))) !== null && _b !== void 0 ? _b : [];
|
|
32
|
+
return [...invalidStyles, ...invalidScripts];
|
|
33
|
+
}
|
|
34
|
+
mapExternalPermissionsToCsp(externalPermissions) {
|
|
35
|
+
var _a;
|
|
36
|
+
const { images, media, scripts, fetch, styles, fonts, frames } = externalPermissions;
|
|
37
|
+
return {
|
|
38
|
+
'img-src': images !== null && images !== void 0 ? images : [],
|
|
39
|
+
'media-src': media !== null && media !== void 0 ? media : [],
|
|
40
|
+
'script-src': scripts !== null && scripts !== void 0 ? scripts : [],
|
|
41
|
+
'style-src': styles !== null && styles !== void 0 ? styles : [],
|
|
42
|
+
'connect-src': (_a = fetch === null || fetch === void 0 ? void 0 : fetch.client) !== null && _a !== void 0 ? _a : [],
|
|
43
|
+
'font-src': fonts !== null && fonts !== void 0 ? fonts : [],
|
|
44
|
+
'frame-src': frames !== null && frames !== void 0 ? frames : []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
getStyleSrc($, userStyleSrc) {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
const quotedUserStyleSrc = (_a = userStyleSrc === null || userStyleSrc === void 0 ? void 0 : userStyleSrc.map((x) => `'${x}'`)) !== null && _a !== void 0 ? _a : [];
|
|
50
|
+
const deprecatedUserScriptSrc = (_b = this.getDeprecatedUserCsp($)['style-src']) !== null && _b !== void 0 ? _b : [];
|
|
51
|
+
const uniqueStyleSrc = [...new Set([...deprecatedUserScriptSrc, ...quotedUserStyleSrc])];
|
|
52
|
+
return uniqueStyleSrc.filter((x) => this.isValidUserStyleSrc(x));
|
|
53
|
+
}
|
|
54
|
+
getScriptSrc($, userScriptSrc) {
|
|
55
|
+
var _a;
|
|
56
|
+
const generatedScriptHashes = this.getInlineScriptHashes($);
|
|
57
|
+
const quotedUserScriptSrc = (_a = userScriptSrc === null || userScriptSrc === void 0 ? void 0 : userScriptSrc.map((x) => `'${x}'`)) !== null && _a !== void 0 ? _a : [];
|
|
58
|
+
const validUserScriptSrc = quotedUserScriptSrc.filter((x) => this.isValidUserScriptSrc(x));
|
|
59
|
+
const { scriptSrc, userScriptHashes } = this.extractUniqueHashes(validUserScriptSrc, generatedScriptHashes);
|
|
60
|
+
return [...scriptSrc, ...generatedScriptHashes, ...userScriptHashes];
|
|
61
|
+
}
|
|
62
|
+
extractUniqueHashes(userScriptSrc, existingScriptHashes) {
|
|
63
|
+
var _a;
|
|
64
|
+
const userScriptHashes = [];
|
|
65
|
+
const scriptSrc = (_a = userScriptSrc === null || userScriptSrc === void 0 ? void 0 : userScriptSrc.filter((scriptSrc) => {
|
|
66
|
+
const isValidHash = this.isValidHash(scriptSrc);
|
|
67
|
+
if (isValidHash && !existingScriptHashes.includes(scriptSrc)) {
|
|
68
|
+
userScriptHashes.push(scriptSrc);
|
|
69
|
+
}
|
|
70
|
+
return !isValidHash;
|
|
71
|
+
})) !== null && _a !== void 0 ? _a : [];
|
|
72
|
+
return { scriptSrc, userScriptHashes };
|
|
73
|
+
}
|
|
74
|
+
getInlineScriptHashes($) {
|
|
75
|
+
const scriptHashes = $('script:not([src])')
|
|
76
|
+
.map((_index, script) => {
|
|
77
|
+
const html = $(script).html();
|
|
78
|
+
return html && `'sha256-${this.hashScript(html)}'`;
|
|
79
|
+
})
|
|
80
|
+
.get();
|
|
81
|
+
return scriptHashes;
|
|
82
|
+
}
|
|
83
|
+
hashScript(content) {
|
|
84
|
+
const sha256 = crypto_1.default.createHash('sha256');
|
|
85
|
+
return sha256.update(content).digest('base64');
|
|
86
|
+
}
|
|
87
|
+
isValidUserScriptSrc(scriptSrc) {
|
|
88
|
+
if (!this.isSafeCsp(scriptSrc))
|
|
89
|
+
return false;
|
|
90
|
+
return this.isValidHash(scriptSrc) || SCRIPT_SRC_ALLOWLIST.includes(scriptSrc);
|
|
91
|
+
}
|
|
92
|
+
isValidUserStyleSrc(styleSrc) {
|
|
93
|
+
if (!this.isSafeCsp(styleSrc)) {
|
|
94
|
+
this.logger.info('discarding potentially-malicious CSP');
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return STYLE_SRC_ALLOWLIST.includes(styleSrc);
|
|
98
|
+
}
|
|
99
|
+
isSafeCsp(cspString) {
|
|
100
|
+
return /^[a-zA-Z0-9='"+\/ -]*$/.test(cspString);
|
|
101
|
+
}
|
|
102
|
+
isValidHash(cspString) {
|
|
103
|
+
return BASE_64_HASH_PATTERNS.some((pattern) => pattern.test(cspString));
|
|
104
|
+
}
|
|
105
|
+
getDeprecatedUserCsp($) {
|
|
106
|
+
const cspContent = $('meta[http-equiv="Content-Security-Policy"]').attr('content');
|
|
107
|
+
if (!cspContent) {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
if (!this.isSafeCsp(cspContent)) {
|
|
111
|
+
this.logger.info('discarding potentially-malicious CSP');
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
return content_security_policy_parser_1.default(cspContent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.CSPProcessingService = CSPProcessingService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csp/index.ts"],"names":[],"mappings":"AAEA,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC"}
|
package/out/csp/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class EgressFilteringService {
|
|
2
|
+
private readonly URLs;
|
|
3
|
+
private readonly wildcardDomains;
|
|
4
|
+
private readonly allowsEverything;
|
|
5
|
+
constructor(allowList: string[]);
|
|
6
|
+
private safeURL;
|
|
7
|
+
isValidUrl(url: string): boolean;
|
|
8
|
+
private domainCheck;
|
|
9
|
+
private domainIsAllowed;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=egress-filtering-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress-filtering-service.d.ts","sourceRoot":"","sources":["../../src/egress/egress-filtering-service.ts"],"names":[],"mappings":"AAGA,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAU;gBAE/B,SAAS,EAAE,MAAM,EAAE;IAY/B,OAAO,CAAC,OAAO;IAOR,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAQvC,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,eAAe;CAWxB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EgressFilteringService = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const micromatch_1 = tslib_1.__importDefault(require("micromatch"));
|
|
6
|
+
const url_1 = require("url");
|
|
7
|
+
class EgressFilteringService {
|
|
8
|
+
constructor(allowList) {
|
|
9
|
+
this.URLs = allowList
|
|
10
|
+
.filter((domainOrURL) => !domainOrURL.startsWith('*'))
|
|
11
|
+
.map((url) => this.safeURL(url));
|
|
12
|
+
this.wildcardDomains = allowList
|
|
13
|
+
.filter((domainOrURL) => domainOrURL !== '*')
|
|
14
|
+
.map((url) => this.safeURL(url))
|
|
15
|
+
.filter((url) => url.hostname.startsWith('*'));
|
|
16
|
+
this.allowsEverything = allowList.includes('*');
|
|
17
|
+
}
|
|
18
|
+
safeURL(url, defaultProtocol = 'https://') {
|
|
19
|
+
const protocolRegex = /^(.*:\/\/)/;
|
|
20
|
+
return new url_1.URL(protocolRegex.test(url) ? url : `${defaultProtocol}${url}`);
|
|
21
|
+
}
|
|
22
|
+
isValidUrl(url) {
|
|
23
|
+
if (this.allowsEverything) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return this.domainIsAllowed(this.safeURL(url));
|
|
27
|
+
}
|
|
28
|
+
domainCheck(domain, allowList) {
|
|
29
|
+
const hostnameMatchedProtocol = allowList
|
|
30
|
+
.filter((allowed) => allowed.protocol === domain.protocol)
|
|
31
|
+
.map((url) => url.hostname);
|
|
32
|
+
return (micromatch_1.default([domain.hostname], hostnameMatchedProtocol, {
|
|
33
|
+
dot: true
|
|
34
|
+
}).length > 0);
|
|
35
|
+
}
|
|
36
|
+
domainIsAllowed(domain) {
|
|
37
|
+
if (this.domainCheck(domain, this.URLs)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (this.domainCheck(domain, this.wildcardDomains)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.EgressFilteringService = EgressFilteringService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/egress/index.ts"],"names":[],"mappings":"AAEA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/egress/utils.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,qCAAqC,oBAAqB,MAAM,EAAE,KAAG,KAAK,CAAC,MAAM,CA2BtF,CAAC;AAEF,OAAO,EAAE,qCAAqC,EAAE,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sortAndGroupEgressPermissionsByDomain = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const micromatch_1 = tslib_1.__importDefault(require("micromatch"));
|
|
6
|
+
const url_1 = require("url");
|
|
7
|
+
const sortAndGroupEgressPermissionsByDomain = (egressAddresses) => {
|
|
8
|
+
const protocolRegex = /^(.*?:\/\/)/;
|
|
9
|
+
const domainSet = new Set();
|
|
10
|
+
const groupSet = new Set();
|
|
11
|
+
const removeSet = new Set();
|
|
12
|
+
if ((egressAddresses === null || egressAddresses === void 0 ? void 0 : egressAddresses.length) === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
egressAddresses.forEach((item) => {
|
|
16
|
+
const itemWithProtocol = protocolRegex.test(item) ? item : `https://${item}`;
|
|
17
|
+
const url = new url_1.URL(itemWithProtocol);
|
|
18
|
+
if (url.hostname.startsWith('*')) {
|
|
19
|
+
groupSet.add(url.hostname.substring(2));
|
|
20
|
+
removeSet.add('!' + url.hostname);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
domainSet.add(url.hostname);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
if (removeSet.size === 0) {
|
|
27
|
+
return [...domainSet];
|
|
28
|
+
}
|
|
29
|
+
return [...new Set(micromatch_1.default([...domainSet], [...removeSet]).concat([...groupSet]))].sort();
|
|
30
|
+
};
|
|
31
|
+
exports.sortAndGroupEgressPermissionsByDomain = sortAndGroupEgressPermissionsByDomain;
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC"}
|
package/out/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./csp"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./egress"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./types"), exports);
|
package/out/types.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { Permissions } from '@forge/manifest';
|
|
3
|
+
export declare type DocumentBody = string | Buffer;
|
|
4
|
+
export declare type ContentPermissions = NonNullable<Permissions['content']>;
|
|
5
|
+
export declare type ExternalPermissions = NonNullable<Permissions['external']>;
|
|
6
|
+
export declare enum ExternalCspType {
|
|
7
|
+
IMG_SRC = "img-src",
|
|
8
|
+
MEDIA_SRC = "media-src",
|
|
9
|
+
SCRIPT_SRC = "script-src",
|
|
10
|
+
STYLE_SRC = "style-src",
|
|
11
|
+
CONNECT_SRC = "connect-src",
|
|
12
|
+
FONT_SRC = "font-src",
|
|
13
|
+
FRAME_SRC = "frame-src"
|
|
14
|
+
}
|
|
15
|
+
export declare type CSPDetails = Record<ExternalCspType, string[]>;
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,oBAAY,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAE3C,oBAAY,kBAAkB,GAAG,WAAW,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;AACrE,oBAAY,mBAAmB,GAAG,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;AACvE,oBAAY,eAAe;IACzB,OAAO,YAAY;IACnB,SAAS,cAAc;IACvB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;IACrB,SAAS,cAAc;CACxB;AACD,oBAAY,UAAU,GAAG,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC,CAAC"}
|
package/out/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExternalCspType = void 0;
|
|
4
|
+
var ExternalCspType;
|
|
5
|
+
(function (ExternalCspType) {
|
|
6
|
+
ExternalCspType["IMG_SRC"] = "img-src";
|
|
7
|
+
ExternalCspType["MEDIA_SRC"] = "media-src";
|
|
8
|
+
ExternalCspType["SCRIPT_SRC"] = "script-src";
|
|
9
|
+
ExternalCspType["STYLE_SRC"] = "style-src";
|
|
10
|
+
ExternalCspType["CONNECT_SRC"] = "connect-src";
|
|
11
|
+
ExternalCspType["FONT_SRC"] = "font-src";
|
|
12
|
+
ExternalCspType["FRAME_SRC"] = "frame-src";
|
|
13
|
+
})(ExternalCspType = exports.ExternalCspType || (exports.ExternalCspType = {}));
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forge/csp",
|
|
3
|
+
"version": "0.0.0-experimental-d18f8dd",
|
|
4
|
+
"description": "Contains the CSP configuration for Custom UI resources in Forge",
|
|
5
|
+
"main": "out/index.js",
|
|
6
|
+
"author": "Atlassian",
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "yarn run clean && yarn run compile",
|
|
10
|
+
"compile": "tsc -b -v",
|
|
11
|
+
"clean": "rm -rf ./out && rm -f tsconfig.tsbuildinfo"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@forge/cli-shared": "^0.0.0-experimental-d18f8dd",
|
|
15
|
+
"@forge/manifest": "^0.0.0-experimental-d18f8dd",
|
|
16
|
+
"@types/jest": "^26.0.0"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"cheerio": "^0.22.0",
|
|
20
|
+
"content-security-policy-parser": "^0.3.0",
|
|
21
|
+
"micromatch": "^4.0.2"
|
|
22
|
+
}
|
|
23
|
+
}
|