apostrophe 3.63.0 → 3.63.2
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
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.63.2 (2024-03-01)
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
* Always validate that method names passed to the `external-condition` API actually appear in `if` or `requiredIf`
|
|
8
|
+
clauses for the field in question. This fix addresses a serious security risk in which arbitrary methods of
|
|
9
|
+
Apostrophe modules could be called over the network, without arguments, and the results returned to the caller.
|
|
10
|
+
While the lack of arguments mitigates the data exfiltration risk, it is possible to cause data loss by
|
|
11
|
+
invoking the right method. Therefore this is an urgent upgrade for all Apostrophe 3.x users. Our thanks to the Michelin
|
|
12
|
+
penetration test red team for disclosing this vulnerability. All are welcome to disclose security vulnerabilities
|
|
13
|
+
in ApostropheCMS code via [security@apostrophecms.com](mailto:security@apostrophecms.com).
|
|
14
|
+
* Disable the `alwaysIframe` query parameter of the oembed proxy. This feature was never used in Apostrophe core, and could be misused to carry out arbitrary GET requests in the context of an iframe, although it could not be used to exfiltrate any information other than the success or failure of the request, and the request was still performed by the user's browser only. Thanks to the Michelin team.
|
|
15
|
+
* Remove vestigial A2 code relating to polymorphic relationship fields. The code in question had no relevance to the way such a feature would be implemented in A3, and could be used to cause a denial of service by crashing and restarting the process. Thanks to the Michelin team.
|
|
16
|
+
|
|
17
|
+
## 3.63.1 (2024-02-22)
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
* Bump dependency on `sanitize-html` to `^2.12.1` at a minimum, to ensure that `npm update apostrophe` is sufficient to guarantee a security update is installed. This security update prevents specially crafted HTML documents from revealing the existence or non-existence of files on the server. The vulnerability did not expose any other information about those files. Thanks to the [Snyk Security team](https://snyk.io/) for the disclosure and to [Dylan Armstrong](https://dylan.is/) for the fix.
|
|
22
|
+
|
|
3
23
|
## 3.63.0 (2024-02-21)
|
|
4
24
|
|
|
5
25
|
### Adds
|
|
@@ -67,7 +67,9 @@ module.exports = {
|
|
|
67
67
|
//
|
|
68
68
|
// If `options.alwaysIframe` is true, the result is a simple
|
|
69
69
|
// iframe of the URL. If `options.iframeHeight` is set, the iframe
|
|
70
|
-
// has that height in pixels, otherwise it is left to CSS.
|
|
70
|
+
// has that height in pixels, otherwise it is left to CSS. These iframe-related
|
|
71
|
+
// options are not used in core Apostrophe and remain available to project-level
|
|
72
|
+
// application code for backwards compatibility purposes only.
|
|
71
73
|
//
|
|
72
74
|
// The `options` object is passed on to `oembetter.fetch`.
|
|
73
75
|
//
|
|
@@ -127,6 +129,9 @@ module.exports = {
|
|
|
127
129
|
await self.apos.cache.set('@apostrophecms/oembed', key, response, self.options.cacheLifetime);
|
|
128
130
|
return response;
|
|
129
131
|
},
|
|
132
|
+
// Not currently used. Present for backwards compatibility
|
|
133
|
+
// in the event that application code used it directly.
|
|
134
|
+
//
|
|
130
135
|
// Given a URL, return an oembed response for it
|
|
131
136
|
// which just iframes the URL given. Fetches the page
|
|
132
137
|
// first to get the title property.
|
|
@@ -219,8 +224,10 @@ module.exports = {
|
|
|
219
224
|
async query(req) {
|
|
220
225
|
const url = self.apos.launder.string(req.query.url);
|
|
221
226
|
const options = {
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
// This feature is no longer available via the
|
|
228
|
+
// oembed proxy because it posed an SSRF risk and
|
|
229
|
+
// was never used in core Apostrophe widgets
|
|
230
|
+
alwaysIframe: false
|
|
224
231
|
};
|
|
225
232
|
|
|
226
233
|
const result = await self.query(req, url, options);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const _ = require('lodash');
|
|
2
1
|
const migrations = require('./lib/migrations.js');
|
|
3
2
|
|
|
4
3
|
module.exports = {
|
|
@@ -14,23 +13,5 @@ module.exports = {
|
|
|
14
13
|
return {
|
|
15
14
|
...migrations(self)
|
|
16
15
|
};
|
|
17
|
-
},
|
|
18
|
-
routes(self) {
|
|
19
|
-
return {
|
|
20
|
-
post: {
|
|
21
|
-
polymorphicChooserModal(req, res) {
|
|
22
|
-
const limit = self.apos.launder.integer(req.body.limit);
|
|
23
|
-
const field = req.body.field;
|
|
24
|
-
const types = _.map(field.withType, function (name) {
|
|
25
|
-
return self.apos.doc.getManager(name);
|
|
26
|
-
});
|
|
27
|
-
return self.send(req, 'chooserModal', {
|
|
28
|
-
options: self.options,
|
|
29
|
-
limit: limit,
|
|
30
|
-
types: types
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
16
|
}
|
|
36
17
|
};
|
|
@@ -709,6 +709,7 @@ module.exports = {
|
|
|
709
709
|
}
|
|
710
710
|
if (hasParenthesis && !methodKey.endsWith('()')) {
|
|
711
711
|
self.apos.util.warn(`The method "${methodDefinition}" defined in the "${fieldName}" field should be written without argument: "${methodDefinition}()".`);
|
|
712
|
+
methodKey = methodDefinition + '()';
|
|
712
713
|
}
|
|
713
714
|
|
|
714
715
|
const [ methodName, moduleName = fieldModuleName ] = methodDefinition
|
|
@@ -1743,7 +1744,11 @@ module.exports = {
|
|
|
1743
1744
|
const conditionKey = self.apos.launder.string(req.query.conditionKey);
|
|
1744
1745
|
|
|
1745
1746
|
const field = self.getFieldById(fieldId);
|
|
1746
|
-
|
|
1747
|
+
const allowedKeys = getFieldExternalConditionKeys(field);
|
|
1748
|
+
// We must tolerate arguments at this stage as we only warn about them later
|
|
1749
|
+
if (!allowedKeys.includes(conditionKey.replace(/\(.*\)/, '()'))) {
|
|
1750
|
+
throw self.apos.error('forbidden', `${conditionKey} is not registered as an external condition.`);
|
|
1751
|
+
}
|
|
1747
1752
|
try {
|
|
1748
1753
|
const result = await self.evaluateMethod(req, conditionKey, field.name, field.moduleName, docId);
|
|
1749
1754
|
return { result };
|
|
@@ -1776,3 +1781,27 @@ module.exports = {
|
|
|
1776
1781
|
};
|
|
1777
1782
|
}
|
|
1778
1783
|
};
|
|
1784
|
+
|
|
1785
|
+
function getFieldExternalConditionKeys(field) {
|
|
1786
|
+
const conditionTypes = [ 'if', 'requiredIf' ];
|
|
1787
|
+
return [
|
|
1788
|
+
...new Set(conditionTypes.map(conditionType => getConditionTypeExternalConditionKeys(field[conditionType] || {})).flat())
|
|
1789
|
+
];
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function getConditionTypeExternalConditionKeys(conditions) {
|
|
1793
|
+
let results = [];
|
|
1794
|
+
if (conditions.$or) {
|
|
1795
|
+
results = conditions.$or.map(getConditionTypeExternalConditionKeys).flat();
|
|
1796
|
+
}
|
|
1797
|
+
for (const key of Object.keys(conditions)) {
|
|
1798
|
+
if (key === '$or') {
|
|
1799
|
+
results = [ ...results, conditions.$or.map(getConditionTypeExternalConditionKeys).flat() ];
|
|
1800
|
+
} else {
|
|
1801
|
+
if (key.endsWith('()')) {
|
|
1802
|
+
results.push(key);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return results;
|
|
1807
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apostrophe",
|
|
3
|
-
"version": "3.63.
|
|
3
|
+
"version": "3.63.2",
|
|
4
4
|
"description": "The Apostrophe Content Management System.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"regexp-quote": "0.0.0",
|
|
123
123
|
"resolve": "^1.19.0",
|
|
124
124
|
"resolve-from": "^5.0.0",
|
|
125
|
-
"sanitize-html": "^2.
|
|
125
|
+
"sanitize-html": "^2.12.1",
|
|
126
126
|
"sass": "^1.52.3",
|
|
127
127
|
"sass-loader": "^10.1.1",
|
|
128
128
|
"server-destroy": "^1.0.1",
|
package/test/schemas.js
CHANGED
|
@@ -2052,7 +2052,10 @@ describe('Schemas', function() {
|
|
|
2052
2052
|
it('should call the evaluate-external-condition API successfully', async function() {
|
|
2053
2053
|
apos.schema.fieldsById['some-field-id'] = {
|
|
2054
2054
|
name: 'someField',
|
|
2055
|
-
moduleName: 'external-condition'
|
|
2055
|
+
moduleName: 'external-condition',
|
|
2056
|
+
if: {
|
|
2057
|
+
'externalCondition()': 'yes'
|
|
2058
|
+
}
|
|
2056
2059
|
};
|
|
2057
2060
|
|
|
2058
2061
|
const res = await apos.http.get('/api/v1/@apostrophecms/schema/evaluate-external-condition?fieldId=some-field-id&docId=some-doc-id&conditionKey=externalCondition()', {});
|
|
@@ -2062,7 +2065,10 @@ describe('Schemas', function() {
|
|
|
2062
2065
|
it('should warn when an argument is passed in the external condition key via the evaluate-external-condition API', async function() {
|
|
2063
2066
|
apos.schema.fieldsById['some-field-id'] = {
|
|
2064
2067
|
name: 'someField',
|
|
2065
|
-
moduleName: 'external-condition'
|
|
2068
|
+
moduleName: 'external-condition',
|
|
2069
|
+
if: {
|
|
2070
|
+
'externalCondition()': 'yes'
|
|
2071
|
+
}
|
|
2066
2072
|
};
|
|
2067
2073
|
|
|
2068
2074
|
const res = await apos.http.get('/api/v1/@apostrophecms/schema/evaluate-external-condition?fieldId=some-field-id&docId=some-doc-id&conditionKey=externalCondition(letsNotArgue)', {});
|
|
@@ -2081,7 +2087,7 @@ describe('Schemas', function() {
|
|
|
2081
2087
|
await apos.http.get('/api/v1/@apostrophecms/schema/evaluate-external-condition?fieldId=some-field-id&docId=some-doc-id&conditionKey=externalCondition()', {});
|
|
2082
2088
|
} catch (error) {
|
|
2083
2089
|
assert(error.status = 400);
|
|
2084
|
-
assert(error.body.message
|
|
2090
|
+
assert.strictEqual(error.body.message, 'externalCondition() is not registered as an external condition.');
|
|
2085
2091
|
return;
|
|
2086
2092
|
}
|
|
2087
2093
|
throw new Error('should have thrown');
|
|
@@ -2097,7 +2103,7 @@ describe('Schemas', function() {
|
|
|
2097
2103
|
await apos.http.get('/api/v1/@apostrophecms/schema/evaluate-external-condition?fieldId=some-field-id&docId=some-doc-id&conditionKey=unknownMethod()', {});
|
|
2098
2104
|
} catch (error) {
|
|
2099
2105
|
assert(error.status = 400);
|
|
2100
|
-
assert(error.body.message
|
|
2106
|
+
assert.strictEqual(error.body.message, 'unknownMethod() is not registered as an external condition.');
|
|
2101
2107
|
return;
|
|
2102
2108
|
}
|
|
2103
2109
|
throw new Error('should have thrown');
|