@forge/csp 3.2.1-next.0 → 3.2.2-next.0
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 +12 -0
- package/out/csp/csp-injection-service.js +58 -61
- package/out/csp/csp-processing-service.js +34 -35
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @forge/csp
|
|
2
2
|
|
|
3
|
+
## 3.2.2-next.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- add374d: Change FedRAMP avatar urls to the correct URL
|
|
8
|
+
|
|
9
|
+
## 3.2.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- bd8bc91: Removing references to FedRamp sandbox
|
|
14
|
+
|
|
3
15
|
## 3.2.1-next.0
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -14,73 +14,16 @@ const atlassianImageHosts = {
|
|
|
14
14
|
stg: ['https://avatar-management--avatars.us-west-2.staging.public.atl-paas.net', apiGatewayHost['stg']],
|
|
15
15
|
prod: ['https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net', apiGatewayHost['prod']],
|
|
16
16
|
'fedramp-stg': [
|
|
17
|
-
'https://avatar-management--avatars.us-east-1.
|
|
17
|
+
'https://avatar-management--avatars.us-east-1.staging.cdn.atlassian-us-gov-mod.com',
|
|
18
18
|
apiGatewayHost['fedramp-stg']
|
|
19
19
|
],
|
|
20
20
|
'fedramp-prod': [
|
|
21
|
-
'https://avatar-management--avatars.us-east-1.prod.
|
|
21
|
+
'https://avatar-management--avatars.us-east-1.prod.cdn.atlassian-us-gov-mod.com',
|
|
22
22
|
apiGatewayHost['fedramp-prod']
|
|
23
23
|
]
|
|
24
24
|
};
|
|
25
25
|
const gravatarUrl = 'https://secure.gravatar.com';
|
|
26
26
|
class CSPInjectionService {
|
|
27
|
-
constructor() {
|
|
28
|
-
this.getInjectableCSP = ({ existingCSPDetails, microsEnv, tunnelCSPReporterUri, hostname, isFedRAMP }) => {
|
|
29
|
-
const reportUri = tunnelCSPReporterUri || this.getCSPReportUri(microsEnv);
|
|
30
|
-
const defaultSrc = `'self'`;
|
|
31
|
-
const frameAncestors = ["'self'", ...this.getFrameAncestors(microsEnv, hostname)].join(' ');
|
|
32
|
-
const frameSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FRAME_SRC, existingCSPDetails)].join(' ');
|
|
33
|
-
const fontSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FONT_SRC, existingCSPDetails)].join(' ');
|
|
34
|
-
const imgSrc = [
|
|
35
|
-
"'self'",
|
|
36
|
-
'data:',
|
|
37
|
-
'blob:',
|
|
38
|
-
hostname,
|
|
39
|
-
gravatarUrl,
|
|
40
|
-
...atlassianImageHosts[microsEnv],
|
|
41
|
-
...this.getExistingCSPDetails(types_1.ExternalCspType.IMG_SRC, existingCSPDetails)
|
|
42
|
-
]
|
|
43
|
-
.filter((a) => a)
|
|
44
|
-
.join(' ');
|
|
45
|
-
const mediaSrc = [
|
|
46
|
-
"'self'",
|
|
47
|
-
'data:',
|
|
48
|
-
'blob:',
|
|
49
|
-
...this.getExistingCSPDetails(types_1.ExternalCspType.MEDIA_SRC, existingCSPDetails)
|
|
50
|
-
].join(' ');
|
|
51
|
-
const connectSrc = [
|
|
52
|
-
"'self'",
|
|
53
|
-
...this.getConnectSrc(microsEnv, !!tunnelCSPReporterUri),
|
|
54
|
-
...this.getExistingCSPDetails(types_1.ExternalCspType.CONNECT_SRC, existingCSPDetails)
|
|
55
|
-
].join(' ');
|
|
56
|
-
const scriptSrc = [
|
|
57
|
-
"'self'",
|
|
58
|
-
this.getForgeGlobalCSP(microsEnv, isFedRAMP),
|
|
59
|
-
...this.getExistingCSPDetails(types_1.ExternalCspType.SCRIPT_SRC, existingCSPDetails)
|
|
60
|
-
].join(' ');
|
|
61
|
-
const styleSrc = [
|
|
62
|
-
"'self'",
|
|
63
|
-
this.getForgeGlobalCSP(microsEnv, isFedRAMP),
|
|
64
|
-
...this.getExistingCSPDetails(types_1.ExternalCspType.STYLE_SRC, existingCSPDetails)
|
|
65
|
-
].join(' ');
|
|
66
|
-
const navigateTo = ["'self'"];
|
|
67
|
-
return [
|
|
68
|
-
`default-src ${defaultSrc}`,
|
|
69
|
-
`frame-ancestors ${frameAncestors}`,
|
|
70
|
-
`frame-src ${frameSrc}`,
|
|
71
|
-
`font-src ${fontSrc}`,
|
|
72
|
-
`img-src ${imgSrc}`,
|
|
73
|
-
`media-src ${mediaSrc}`,
|
|
74
|
-
`connect-src ${connectSrc}`,
|
|
75
|
-
`script-src ${scriptSrc}`,
|
|
76
|
-
`navigate-to ${navigateTo}`,
|
|
77
|
-
`style-src ${styleSrc}`,
|
|
78
|
-
`form-action 'self'`,
|
|
79
|
-
`sandbox allow-downloads allow-forms allow-modals allow-pointer-lock allow-same-origin allow-scripts`,
|
|
80
|
-
`report-uri ${reportUri}`
|
|
81
|
-
];
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
27
|
getCSPReportUri(microsEnv) {
|
|
85
28
|
if (microsEnv === 'dev' || microsEnv === 'stg')
|
|
86
29
|
return 'https://web-security-reports.stg.services.atlassian.com/csp-report/forge-cdn';
|
|
@@ -92,8 +35,7 @@ class CSPInjectionService {
|
|
|
92
35
|
: `https://forge.cdn.${microsEnv}.atlassian-dev.net`;
|
|
93
36
|
}
|
|
94
37
|
getExistingCSPDetails(cspType, cspDetails) {
|
|
95
|
-
|
|
96
|
-
return (_a = cspDetails[cspType]) !== null && _a !== void 0 ? _a : [];
|
|
38
|
+
return cspDetails[cspType] ?? [];
|
|
97
39
|
}
|
|
98
40
|
getConnectSrc(microsEnv, isTunnelling) {
|
|
99
41
|
const allowed = [];
|
|
@@ -130,5 +72,60 @@ class CSPInjectionService {
|
|
|
130
72
|
}
|
|
131
73
|
return frameAncestors;
|
|
132
74
|
}
|
|
75
|
+
getInjectableCSP = ({ existingCSPDetails, microsEnv, tunnelCSPReporterUri, hostname, isFedRAMP }) => {
|
|
76
|
+
const reportUri = tunnelCSPReporterUri || this.getCSPReportUri(microsEnv);
|
|
77
|
+
const defaultSrc = `'self'`;
|
|
78
|
+
const frameAncestors = ["'self'", ...this.getFrameAncestors(microsEnv, hostname)].join(' ');
|
|
79
|
+
const frameSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FRAME_SRC, existingCSPDetails)].join(' ');
|
|
80
|
+
const fontSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FONT_SRC, existingCSPDetails)].join(' ');
|
|
81
|
+
const imgSrc = [
|
|
82
|
+
"'self'",
|
|
83
|
+
'data:',
|
|
84
|
+
'blob:',
|
|
85
|
+
hostname,
|
|
86
|
+
gravatarUrl,
|
|
87
|
+
...atlassianImageHosts[microsEnv],
|
|
88
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.IMG_SRC, existingCSPDetails)
|
|
89
|
+
]
|
|
90
|
+
.filter((a) => a)
|
|
91
|
+
.join(' ');
|
|
92
|
+
const mediaSrc = [
|
|
93
|
+
"'self'",
|
|
94
|
+
'data:',
|
|
95
|
+
'blob:',
|
|
96
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.MEDIA_SRC, existingCSPDetails)
|
|
97
|
+
].join(' ');
|
|
98
|
+
const connectSrc = [
|
|
99
|
+
"'self'",
|
|
100
|
+
...this.getConnectSrc(microsEnv, !!tunnelCSPReporterUri),
|
|
101
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.CONNECT_SRC, existingCSPDetails)
|
|
102
|
+
].join(' ');
|
|
103
|
+
const scriptSrc = [
|
|
104
|
+
"'self'",
|
|
105
|
+
this.getForgeGlobalCSP(microsEnv, isFedRAMP),
|
|
106
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.SCRIPT_SRC, existingCSPDetails)
|
|
107
|
+
].join(' ');
|
|
108
|
+
const styleSrc = [
|
|
109
|
+
"'self'",
|
|
110
|
+
this.getForgeGlobalCSP(microsEnv, isFedRAMP),
|
|
111
|
+
...this.getExistingCSPDetails(types_1.ExternalCspType.STYLE_SRC, existingCSPDetails)
|
|
112
|
+
].join(' ');
|
|
113
|
+
const navigateTo = ["'self'"];
|
|
114
|
+
return [
|
|
115
|
+
`default-src ${defaultSrc}`,
|
|
116
|
+
`frame-ancestors ${frameAncestors}`,
|
|
117
|
+
`frame-src ${frameSrc}`,
|
|
118
|
+
`font-src ${fontSrc}`,
|
|
119
|
+
`img-src ${imgSrc}`,
|
|
120
|
+
`media-src ${mediaSrc}`,
|
|
121
|
+
`connect-src ${connectSrc}`,
|
|
122
|
+
`script-src ${scriptSrc}`,
|
|
123
|
+
`navigate-to ${navigateTo}`,
|
|
124
|
+
`style-src ${styleSrc}`,
|
|
125
|
+
`form-action 'self'`,
|
|
126
|
+
`sandbox allow-downloads allow-forms allow-modals allow-pointer-lock allow-same-origin allow-scripts`,
|
|
127
|
+
`report-uri ${reportUri}`
|
|
128
|
+
];
|
|
129
|
+
};
|
|
133
130
|
}
|
|
134
131
|
exports.CSPInjectionService = CSPInjectionService;
|
|
@@ -12,36 +12,39 @@ class InvalidConnectSrc extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
exports.InvalidConnectSrc = InvalidConnectSrc;
|
|
14
14
|
class CSPProcessingService {
|
|
15
|
+
logger;
|
|
16
|
+
STYLE_SRC_ALLOWLIST = [`'unsafe-inline'`];
|
|
17
|
+
QUOTED_SCRIPT_SRC_ALLOWLIST = ['unsafe-inline', 'unsafe-eval', 'unsafe-hashes'];
|
|
18
|
+
UNQUOTED_SCRIPT_SRC_ALLOWLIST = ['blob:'];
|
|
19
|
+
SCRIPT_SRC_ALLOWLIST = [...this.QUOTED_SCRIPT_SRC_ALLOWLIST, ...this.UNQUOTED_SCRIPT_SRC_ALLOWLIST];
|
|
20
|
+
BASE_64_HASH_PATTERNS = [
|
|
21
|
+
/^sha256-[a-zA-Z0-9=+/]{44}$/,
|
|
22
|
+
/^sha384-[a-zA-Z0-9=+/]{64}$/,
|
|
23
|
+
/^sha512-[a-zA-Z0-9=+/]{88}$/
|
|
24
|
+
];
|
|
15
25
|
constructor(logger) {
|
|
16
26
|
this.logger = logger;
|
|
17
|
-
this.STYLE_SRC_ALLOWLIST = [`'unsafe-inline'`];
|
|
18
|
-
this.QUOTED_SCRIPT_SRC_ALLOWLIST = ['unsafe-inline', 'unsafe-eval', 'unsafe-hashes'];
|
|
19
|
-
this.UNQUOTED_SCRIPT_SRC_ALLOWLIST = ['blob:'];
|
|
20
|
-
this.SCRIPT_SRC_ALLOWLIST = [...this.QUOTED_SCRIPT_SRC_ALLOWLIST, ...this.UNQUOTED_SCRIPT_SRC_ALLOWLIST];
|
|
21
|
-
this.BASE_64_HASH_PATTERNS = [
|
|
22
|
-
/^sha256-[a-zA-Z0-9=+/]{44}$/,
|
|
23
|
-
/^sha384-[a-zA-Z0-9=+/]{64}$/,
|
|
24
|
-
/^sha512-[a-zA-Z0-9=+/]{88}$/
|
|
25
|
-
];
|
|
26
27
|
}
|
|
27
28
|
getCspDetails(body, permissions) {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const external = (_b = permissions === null || permissions === void 0 ? void 0 : permissions.external) !== null && _b !== void 0 ? _b : {};
|
|
29
|
+
const { scripts, styles } = permissions?.content ?? { scripts: [], styles: [] };
|
|
30
|
+
const external = permissions?.external ?? {};
|
|
31
31
|
const $ = cheerio_1.default.load(body);
|
|
32
|
-
const
|
|
33
|
-
return
|
|
32
|
+
const { 'script-src': scriptSrc, 'style-src': styleSrc, ...mappedExternalCsp } = this.mapExternalPermissionsToCsp(external);
|
|
33
|
+
return {
|
|
34
|
+
'style-src': [...this.getStyleSrc($, styles), ...styleSrc],
|
|
35
|
+
'script-src': [...this.getScriptSrc($, scripts), ...scriptSrc],
|
|
36
|
+
...mappedExternalCsp
|
|
37
|
+
};
|
|
34
38
|
}
|
|
35
39
|
getInvalidCspPermissions(contentPermissions) {
|
|
36
|
-
var _a, _b;
|
|
37
40
|
const { styles, scripts } = contentPermissions;
|
|
38
|
-
const invalidStyles =
|
|
39
|
-
const invalidScripts =
|
|
41
|
+
const invalidStyles = styles?.filter((styleSrc) => !this.isValidUserStyleSrc(`'${styleSrc}'`)) ?? [];
|
|
42
|
+
const invalidScripts = scripts?.filter((scriptSrc) => !this.isValidUserScriptSrc(scriptSrc)) ?? [];
|
|
40
43
|
return [...invalidStyles, ...invalidScripts];
|
|
41
44
|
}
|
|
42
45
|
assertValidFetchClient(fetch) {
|
|
43
|
-
if (fetch
|
|
44
|
-
for (const client of fetch
|
|
46
|
+
if (fetch?.client) {
|
|
47
|
+
for (const client of fetch?.client) {
|
|
45
48
|
if (typeof client !== 'string') {
|
|
46
49
|
throw new InvalidConnectSrc();
|
|
47
50
|
}
|
|
@@ -49,43 +52,39 @@ class CSPProcessingService {
|
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
mapExternalPermissionsToCsp(externalPermissions) {
|
|
52
|
-
var _a;
|
|
53
55
|
const { images, media, scripts, fetch, styles, fonts, frames } = externalPermissions;
|
|
54
56
|
this.assertValidFetchClient(fetch);
|
|
55
57
|
return {
|
|
56
|
-
'img-src': images
|
|
57
|
-
'media-src': media
|
|
58
|
-
'script-src': scripts
|
|
59
|
-
'style-src': styles
|
|
60
|
-
'connect-src':
|
|
61
|
-
'font-src': fonts
|
|
62
|
-
'frame-src': frames
|
|
58
|
+
'img-src': images ?? [],
|
|
59
|
+
'media-src': media ?? [],
|
|
60
|
+
'script-src': scripts ?? [],
|
|
61
|
+
'style-src': styles ?? [],
|
|
62
|
+
'connect-src': fetch?.client ?? [],
|
|
63
|
+
'font-src': fonts ?? [],
|
|
64
|
+
'frame-src': frames ?? []
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
getStyleSrc($, userStyleSrc) {
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const deprecatedUserStyleSrc = (_b = this.getDeprecatedUserCsp($)['style-src']) !== null && _b !== void 0 ? _b : [];
|
|
68
|
+
const quotedUserStyleSrc = userStyleSrc?.map((x) => `'${x}'`) ?? [];
|
|
69
|
+
const deprecatedUserStyleSrc = this.getDeprecatedUserCsp($)['style-src'] ?? [];
|
|
69
70
|
const uniqueStyleSrc = [...new Set([...deprecatedUserStyleSrc, ...quotedUserStyleSrc])];
|
|
70
71
|
return uniqueStyleSrc.filter((x) => this.isValidUserStyleSrc(x));
|
|
71
72
|
}
|
|
72
73
|
getScriptSrc($, userScriptSrc) {
|
|
73
|
-
|
|
74
|
-
const validUserScriptSrc = (_a = userScriptSrc === null || userScriptSrc === void 0 ? void 0 : userScriptSrc.filter((x) => this.isValidUserScriptSrc(x))) !== null && _a !== void 0 ? _a : [];
|
|
74
|
+
const validUserScriptSrc = userScriptSrc?.filter((x) => this.isValidUserScriptSrc(x)) ?? [];
|
|
75
75
|
const generatedScriptHashes = validUserScriptSrc.includes('unsafe-inline') ? [] : this.getInlineScriptHashes($);
|
|
76
76
|
const { scriptSrc, userScriptHashes } = this.extractUniqueHashes(validUserScriptSrc, generatedScriptHashes);
|
|
77
77
|
return [...scriptSrc, ...generatedScriptHashes, ...userScriptHashes].map((x) => this.formatScriptSrc(x));
|
|
78
78
|
}
|
|
79
79
|
extractUniqueHashes(userScriptSrc, existingScriptHashes) {
|
|
80
|
-
var _a;
|
|
81
80
|
const userScriptHashes = [];
|
|
82
|
-
const scriptSrc =
|
|
81
|
+
const scriptSrc = userScriptSrc?.filter((scriptSrc) => {
|
|
83
82
|
const isValidHash = this.isValidHash(scriptSrc);
|
|
84
83
|
if (isValidHash && !existingScriptHashes.includes(scriptSrc)) {
|
|
85
84
|
userScriptHashes.push(scriptSrc);
|
|
86
85
|
}
|
|
87
86
|
return !isValidHash;
|
|
88
|
-
})
|
|
87
|
+
}) ?? [];
|
|
89
88
|
return { scriptSrc, userScriptHashes };
|
|
90
89
|
}
|
|
91
90
|
getInlineScriptHashes($) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forge/csp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2-next.0",
|
|
4
4
|
"description": "Contains the CSP configuration for Custom UI resources in Forge",
|
|
5
5
|
"main": "out/index.js",
|
|
6
6
|
"author": "Atlassian",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"clean": "rm -rf ./out && rm -f tsconfig.tsbuildinfo"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@forge/cli-shared": "
|
|
15
|
-
"@forge/manifest": "
|
|
14
|
+
"@forge/cli-shared": "5.1.1-next.1",
|
|
15
|
+
"@forge/manifest": "7.5.0-next.1",
|
|
16
16
|
"@types/jest": "^29.5.12",
|
|
17
17
|
"@types/node": "14.18.63"
|
|
18
18
|
},
|