apostrophe 3.63.1 → 3.63.3
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 +27 -0
- package/defaults.js +1 -0
- package/modules/@apostrophecms/multisite-i18n/i18n/aposMultisite/en.json +48 -0
- package/modules/@apostrophecms/multisite-i18n/index.js +7 -0
- package/modules/@apostrophecms/oembed/index.js +10 -3
- package/modules/@apostrophecms/polymorphic-type/index.js +0 -19
- package/modules/@apostrophecms/schema/index.js +30 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +1 -1
- package/package.json +1 -1
- package/test/schemas.js +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.63.3 (2024-03-14)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* Add translation keys used by the multisite assembly module. This was released ahead of
|
|
8
|
+
our regular schedule because the multisite module was released early with the expectation
|
|
9
|
+
that these keys would be present.
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
* `field.help` and `field.htmlHelp` are now correctly translated when displayed in a tooltip.
|
|
14
|
+
This was also an expectation for the multisite module.
|
|
15
|
+
|
|
16
|
+
## 3.63.2 (2024-03-01)
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
* Always validate that method names passed to the `external-condition` API actually appear in `if` or `requiredIf`
|
|
21
|
+
clauses for the field in question. This fix addresses a serious security risk in which arbitrary methods of
|
|
22
|
+
Apostrophe modules could be called over the network, without arguments, and the results returned to the caller.
|
|
23
|
+
While the lack of arguments mitigates the data exfiltration risk, it is possible to cause data loss by
|
|
24
|
+
invoking the right method. Therefore this is an urgent upgrade for all Apostrophe 3.x users. Our thanks to the Michelin
|
|
25
|
+
penetration test red team for disclosing this vulnerability. All are welcome to disclose security vulnerabilities
|
|
26
|
+
in ApostropheCMS code via [security@apostrophecms.com](mailto:security@apostrophecms.com).
|
|
27
|
+
* 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.
|
|
28
|
+
* 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.
|
|
29
|
+
|
|
3
30
|
## 3.63.1 (2024-02-22)
|
|
4
31
|
|
|
5
32
|
### Security
|
package/defaults.js
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"shortName": "Short Name",
|
|
3
|
+
"shortNameHelp": "If the short name is \"niftypig\", then the temporary hostname of the site will be \"niftypig.{{ baseDomain }}\".",
|
|
4
|
+
"prodHostname": "Production Hostname",
|
|
5
|
+
"prodHostnameHelp": "We will also automatically add \"www.\" as an alternate. The final name of the site. Do not add unless the DNS is being changed or has been changed to point to this service",
|
|
6
|
+
"canonicalize": "Redirect to Production Hostname",
|
|
7
|
+
"canonicalizeHelp": "Do not activate this until you see that both DNS and HTTPS are working for the production hostname.",
|
|
8
|
+
"canonicalizeStatus": "Canonical Redirect Status Code",
|
|
9
|
+
"canonicalizeStatusHelp": "\"Moved Permanently\" is best for SEO, but you should make sure you are happy with the results using \"Moved Temporarily\" first to avoid caching of bad redirects.",
|
|
10
|
+
"hostnamesArray": "Hostnames",
|
|
11
|
+
"hostnamesArrayHelp": "All valid hostnames for the site must be on this list, for instance both example.com and www.example.com",
|
|
12
|
+
"devBaseUrl": "Development Base URL",
|
|
13
|
+
"devBaseUrlHelp": "like http://localhost:3000",
|
|
14
|
+
"stagingBaseUrl": "Staging Base URL",
|
|
15
|
+
"stagingBaseUrlHelp": "like http://project.staging.org",
|
|
16
|
+
"prodBaseUrl": "Production Base URL",
|
|
17
|
+
"prodBaseUrlHelp": "like https://myproject.com",
|
|
18
|
+
"localeName": "Name",
|
|
19
|
+
"localeNameHelp": "Like en or en-GB. NOTE: the name may be changed but renaming a locale can be a slow operation. Consider changing just the label, prefix and hostname.",
|
|
20
|
+
"localeLabel": "Label",
|
|
21
|
+
"localeLabelHelp": "Like British English",
|
|
22
|
+
"localePrefix": "Prefix",
|
|
23
|
+
"localePrefixHelp": "Like /en",
|
|
24
|
+
"localeSeparateHost": "Separate Host",
|
|
25
|
+
"localeSeparateHostHelp": "This locale requires a separate hostname, e.g. fr.example.com in staging or example.fr in production.",
|
|
26
|
+
"localeStagingSubdomain": "Staging Subdomain",
|
|
27
|
+
"localeStagingSubdomainHelp": "Custom subdomain used in staging. Multiple locales can be configured with the same subdomain in order to group them on it, for instance \"canada.example.com/en\" and \"canada.example.com/fr\". Note that all but one locale must have a prefix for distinction. If left blank, the locale name will be used as the subdomain, outside of production.",
|
|
28
|
+
"localeSeparateProductionHostname": "Separate Production Hostname",
|
|
29
|
+
"localeSeparateProductionHostnameHelp": "Like example.fr. If not set, defaults to LOCALE.SHORTNAME.{{ baseDomain }}, e.g. fr.somesite.{{ baseDomain }}.",
|
|
30
|
+
"localePrivate": "Private locale",
|
|
31
|
+
"localePrivateHelp": "This locale is private",
|
|
32
|
+
"adminPassword": "Admin Password",
|
|
33
|
+
"adminPasswordHelp": "Set password for the \"admin\" user of the new site. For pre-existing sites, leave blank for no change.",
|
|
34
|
+
"redirect": "Redirect Entire Site",
|
|
35
|
+
"redirectHelp": "Redirect all traffic for the site to another URL.",
|
|
36
|
+
"redirectUrl": "Redirect To...",
|
|
37
|
+
"redirectUrlHelp": "Redirect traffic to this URL.",
|
|
38
|
+
"redirectPreservePath": "Preserve the Path when Redirecting",
|
|
39
|
+
"redirectPreservePathHelp": "If the URL ends with /something, add /something to the redirect URL as well. Otherwise, all traffic is redirected to a single place.",
|
|
40
|
+
"redirectStatus": "Redirect Status Code",
|
|
41
|
+
"redirectStatusHelp": "\"Moved Permanently\" is best for SEO, but you should test thoroughly first with \"Moved Temporarily\" to avoid caching of bad redirects.",
|
|
42
|
+
"emptyAdminPasswordError": "You must fill out the admin password field.",
|
|
43
|
+
"shortnameError": "The short name of the site must not contain dots, a protocol or spaces. It is a short name like \"nifty\" (without quotes) and will be used as a \"working name\" for a temporary subdomain for your site until it is launched.",
|
|
44
|
+
"shortnameInUseError": "That short name is already in use by another site.",
|
|
45
|
+
"productionHostnameInUseError": "That Production Hostname is already in use by another site.",
|
|
46
|
+
"renamingLocale": "Renaming locale {{ oldName }} to {{ newName }} in site {{ siteName }}, access to the site is paused, this may take time",
|
|
47
|
+
"localeRenamed": "Locale renamed"
|
|
48
|
+
}
|
|
@@ -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
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');
|