apostrophe 4.31.0-alpha.1 → 4.31.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/.claude/settings.local.json +15 -0
- package/CHANGELOG.md +25 -65
- package/modules/@apostrophecms/file/index.js +9 -8
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +3 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -0
- package/modules/@apostrophecms/util/index.js +17 -0
- package/package.json +9 -9
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/files.js +28 -0
- package/test/utils.js +103 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(timeout 180 npx mocha:*)",
|
|
5
|
+
"Bash(timeout 600 npx mocha:*)",
|
|
6
|
+
"Bash(npm ls:*)",
|
|
7
|
+
"Bash(timeout 540 npx mocha:*)",
|
|
8
|
+
"Bash(echo:*)",
|
|
9
|
+
"Bash(timeout 10 node:*)",
|
|
10
|
+
"Bash(timeout 300 npx mocha:*)",
|
|
11
|
+
"Bash(timeout 60 npx mocha:*)",
|
|
12
|
+
"Bash(timeout 120 npx mocha:*)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,70 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 4.31.0
|
|
4
|
-
|
|
5
|
-
###
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- 2e2f3b4: Accessibility: the Recently Edited Documents tray icon (admin bar) now exposes its action through `aria-label`.
|
|
29
|
-
- 2e2f3b4: Accessibility: fix `.apos-sr-only` so screen-reader-only content is exposed to the accessibility tree.
|
|
30
|
-
- 2e2f3b4: Accessibility: icon-only context-utility buttons in the admin bar tray (e.g. the global settings cog) now expose their action through `aria-label`.
|
|
31
|
-
- 41670a3: Security fix: the password reset request feature will refuse to operate unless the baseUrl option or the APOS_BASE_URL environment variable has been set (note that this is automatic in multisite projects). This fix is necessary to prevent a vulnerability that can be used to convince ApostropheCMS to send emails containing links to other sites. It is only a vulnerability if you have enabled the passwordReset: true option for the login module. Thanks to [SPIDY](https://github.com/Mujahidkhan525) for reporting the issue.
|
|
32
|
-
- 9f458b5: Fix illegal HTML id attribute values generated by the admin UI
|
|
33
|
-
- 08845c5: - Removed duplicate <meta charset> tag from `outerLayoutBase.html`
|
|
34
|
-
- Standardized charset to utf-8 (the legacy configuration option is now ignored). Per the spec this is the only legal setting, so we classify this as a bug fix
|
|
35
|
-
- Altered unused/legacy i18n template helper to return `utf-8`, ensuring backwards compatibility
|
|
36
|
-
- b9b32bd: Keyboard shortcuts for widget operations (copy, cut, paste, duplicate, remove) no longer block the browser's native clipboard behavior when no widget is focused. Previously, selecting and copying text on a page while logged in was prevented by the admin UI intercepting those shortcuts unconditionally.
|
|
37
|
-
- 2191c8a: Fix more admin UI a11y issues
|
|
38
|
-
- 08dfcac: Security: a malicious full name containing HTML was executed as HTML in the tooltip displayed with an "i" icon next to the title of the current page, creating an
|
|
39
|
-
XSS attack risk versus other users. Since most projects permit users to change their full name (the "title" property), All projects with multiple users should be
|
|
40
|
-
updated promptly to close this vulnerability. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
|
|
41
|
-
- 5d1a028: Security: launder now uses and exports the best available naughtyHref function for detecting malicious URLs. sanitize-html now depends on it, and apostrophe now uses type: 'url' for the link URL field of image widgets, which leverages it. Prior to this fix, it was possible for any user with editing privileges, including a contributor, to trigger arbitrary JavaScript via a javascript: URL in the link URL field of an image widget. A migration has been included to strip any such malicious URLs already present in the database. All users of apostrophe are encouraged to upgrade to get this security fix. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
|
|
42
|
-
- 8098017: Security: the HTML import feature of the rich text widget no longer permits images to be fetched
|
|
43
|
-
from arbitrary hosts. This could be used to probe internal networks, and to exfiltrate images from
|
|
44
|
-
internal hosts if their URLs were known. Instead, the `imageImportAllowedHostnames` option of the
|
|
45
|
-
`@apostrophecms/rich-text-widget` must be configured to opt into that feature.
|
|
46
|
-
|
|
47
|
-
Thanks to [Yiğit Şengezer](https://github.com/yigitsengezer) and [Sainithin0309](https://github.com/Sainithin0309) for reporting this issue.
|
|
48
|
-
|
|
49
|
-
- b7f9ad5: Sites with a custom filterByIndexPage method no longer experience failures in the sitemap module and potential creeping CPU performance penalties. A regression introduced with our static site support, but not specific to static sites.
|
|
50
|
-
- b360b05: fixes issue where orderable table array items drag the entire floating window
|
|
51
|
-
- e9b3bac: apostrophe and oembetter have been updated to eliminate a number of services that formerly supported
|
|
52
|
-
oembed for the general public, but no longer do so. While there is no security risk today, removing
|
|
53
|
-
these ensures that if these domains are ever allowed to lapse, they do not become an XSS
|
|
54
|
-
attack vector in the future.
|
|
55
|
-
|
|
56
|
-
Because oembed responses are not always iframes, it is important that this list be maintained
|
|
57
|
-
over time. In addition, developers always have the option to prune it on their own by setting
|
|
58
|
-
the new minimumAllowlist and minimumEndpoints options of the @apostrophecms/oembed module.
|
|
59
|
-
|
|
60
|
-
Thanks to [Sainithin0309](https://github.com/Sainithin0309) for pointing out the potential
|
|
61
|
-
long-term security concern.
|
|
62
|
-
|
|
63
|
-
- Updated dependencies [862d760]
|
|
64
|
-
- Updated dependencies [5d1a028]
|
|
65
|
-
- Updated dependencies [8d4c882]
|
|
66
|
-
- sanitize-html@2.17.5
|
|
67
|
-
- launder@1.7.2
|
|
3
|
+
## 4.31.0 (2026-06-10)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
- Added support for `draggable: false` on non-inline `array` schema fields. Previously this option was only respected when `inline: true`. When set on a standard (modal-based) array field, drag-and-drop reordering and keyboard reordering are now disabled in the array editor's slat list.
|
|
8
|
+
- Introduced support for postgres://, sqlite://, and multipostgres:// database URIs in addition to mongodb://. The new db-connect API supports all of the database operations currently used in our own core, pro and multisite modules. For more information see the documentation.
|
|
9
|
+
- JSX support for templates within ApostropheCMS. JSX is now co-equal with Nunjucks, with a gradual migration strategy. Anyone who is familiar with React will be very comfortable writing JSX templates, which also offer a superior debugging experience, and templates can be migrated gradually. JSX is a great option for those who don't wish to create parallel Astro and ApostropheCMS projects, but still prefer a modern syntax. For more information, see the new [JSX templates guide](https://apostrophecms.com/docs/guide/jsx-templates.html).
|
|
10
|
+
- The session secret and the uploadfs `disabledFileKey` can now be supplied via the `APOS_SESSION_SECRET` and `APOS_UPLOADFS_DISABLED_FILE_KEY` environment variables. As with other Apostrophe environment variables, these take precedence over the corresponding `app.js` configuration.
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- Fixed an issue where using the Tab key to navigate within modals could incorrectly jump focus to a wrong element instead of the next input field.
|
|
15
|
+
Fixed Tab navigation escaping out of modals when the form contained hidden sections or elements that became disabled after editing.
|
|
16
|
+
- Fixed adding or removing an area field from a schema breaking existing documents on an external front such as Astro.
|
|
17
|
+
- For Astro: `AposArea` now renders only schema-backed areas. A missing area no longer throws, and an area orphaned by removing its field from the schema (while its content remains in the document) renders nothing instead of breaking sibling areas in edit mode. Logged-in editors get a diagnostic message in place of an orphaned area; anonymous visitors see nothing.
|
|
18
|
+
- Editable documents sent to an external front (Asgtro) now materialize empty area objects for schema area fields added after the document was created, so they can be edited in context.
|
|
19
|
+
- `apos.util.getManagerOf` accepts a `{ log }` option to suppress its error log when probing objects that may not have a manager.
|
|
20
|
+
- Fix more admin UI a11y issues.
|
|
21
|
+
- Selecting an item in a relationship "browse" dialog no longer scrolls the title and Cancel/Select buttons out of view when the item is far down the list.
|
|
22
|
+
- Sites with a custom filterByIndexPage method no longer experience failures in the sitemap module and potential creeping CPU performance penalties. A regression introduced with our static site support, but not specific to static sites.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- Server-side prototype pollution (CWE-1321) via dot-notation paths. `apos.util.set()` and `apos.util.get()` now refuse to traverse `__proto__`, `constructor` and `prototype` path segments. Previously an authenticated editor could send a PATCH REST API request whose patch operators (for example `$pullAll` with a key of `__proto__.publicApiProjection`) wrote to `Object.prototype`. A polluted `publicApiProjection` defeated the `publicApiCheck()` authorization gate on piece-type REST endpoints for subsequent unauthenticated requests, for the lifetime of the Node.js process. All users should update. Thanks to [tonghuaroot](https://github.com/tonghuaroot), [H3xV0rT3x](https://github.com/H3xV0rT3x), and [5h1kh4r](https://github.com/5h1kh4r) for reporting the vulnerability.
|
|
27
|
+
- When `@apostrophecms/file` pretty URLs are enabled (`prettyUrls: true`), the upstream request used to serve the file is no longer built from the incoming `Host` header. The self-request is now resolved against the site's configured `baseUrl` (via `req.baseUrl`), falling back to the request host only when no `baseUrl` is configured. This closes a server-side request forgery (SSRF) vector in which the `Host` header could steer the proxied fetch at another host. The real-world risk was low: the path is constrained to an existing attachment's `/uploads/attachments/<cuid>-<slug>.<ext>`, and cuids are unique and immutable, so any reachable content was already public via the front door. Thanks to [EchoSkorJjj](https://github.com/EchoSkorJjj) for reporting the issue.
|
|
68
28
|
|
|
69
29
|
## 4.30.0
|
|
70
30
|
|
|
@@ -248,14 +248,15 @@ module.exports = {
|
|
|
248
248
|
const uglyUrl = self.apos.attachment.url(file.attachment, {
|
|
249
249
|
prettyUrl: false
|
|
250
250
|
});
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// `
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
251
|
+
// `uglyUrl` may be relative (local uploadfs) or absolute
|
|
252
|
+
// (S3/CDN). For the relative case `streamProxy` resolves it
|
|
253
|
+
// against `req.baseUrl`, which reflects the configured
|
|
254
|
+
// `baseUrl` (or locale hostname) and only falls back to the
|
|
255
|
+
// request host when the site has none. We deliberately do
|
|
256
|
+
// not build the upstream URL from the raw `Host` header
|
|
257
|
+
// here, as that is attacker-controlled and would allow the
|
|
258
|
+
// self-request to be redirected to an arbitrary host (SSRF).
|
|
259
|
+
return await streamProxy(req, uglyUrl, { error: self.apos.util.error });
|
|
259
260
|
} catch (e) {
|
|
260
261
|
self.apos.util.error('Error in pretty URL route:', e);
|
|
261
262
|
return res.status(500).send('error');
|
|
@@ -252,6 +252,9 @@ export default {
|
|
|
252
252
|
@include apos-transition();
|
|
253
253
|
|
|
254
254
|
& {
|
|
255
|
+
// Contain the visually hidden (`.apos-sr-only`, position: absolute)
|
|
256
|
+
// file input so focusing it does not scroll a distant ancestor.
|
|
257
|
+
position: relative;
|
|
255
258
|
display: flex;
|
|
256
259
|
box-sizing: border-box;
|
|
257
260
|
align-items: center;
|
|
@@ -35,6 +35,13 @@ const util = require('util');
|
|
|
35
35
|
const { stripIndent } = require('common-tags');
|
|
36
36
|
const glob = require('../../../lib/glob.js');
|
|
37
37
|
|
|
38
|
+
// Dot-path segments that must never be traversed when walking a
|
|
39
|
+
// user-supplied path in `apos.util.get` and `apos.util.set`. Following any
|
|
40
|
+
// of these reaches the prototype chain and enables server-side prototype
|
|
41
|
+
// pollution (CWE-1321), e.g. a PATCH `$pullAll` key of
|
|
42
|
+
// `__proto__.publicApiProjection`.
|
|
43
|
+
const unsafePathSegments = new Set([ '__proto__', 'constructor', 'prototype' ]);
|
|
44
|
+
|
|
38
45
|
module.exports = {
|
|
39
46
|
options: {
|
|
40
47
|
alias: 'util',
|
|
@@ -755,6 +762,10 @@ module.exports = {
|
|
|
755
762
|
if (o == null) {
|
|
756
763
|
return undefined;
|
|
757
764
|
}
|
|
765
|
+
if (unsafePathSegments.has(p)) {
|
|
766
|
+
// Never read through the prototype chain (CWE-1321)
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
758
769
|
o = o[p];
|
|
759
770
|
}
|
|
760
771
|
}
|
|
@@ -819,6 +830,12 @@ module.exports = {
|
|
|
819
830
|
}
|
|
820
831
|
}
|
|
821
832
|
path = path.split('.');
|
|
833
|
+
for (p of path) {
|
|
834
|
+
if (unsafePathSegments.has(p)) {
|
|
835
|
+
// Refuse to write through the prototype chain (CWE-1321)
|
|
836
|
+
throw self.apos.error('invalid', `Unsafe property name "${p}" in dot path`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
822
839
|
for (i = 0; (i < (path.length - 1)); i++) {
|
|
823
840
|
p = path[i];
|
|
824
841
|
o = o[p];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apostrophe",
|
|
3
|
-
"version": "4.31.0
|
|
3
|
+
"version": "4.31.0",
|
|
4
4
|
"description": "The Apostrophe Content Management System.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -120,15 +120,15 @@
|
|
|
120
120
|
"webpack": "^5.106.1",
|
|
121
121
|
"webpack-merge": "^5.7.3",
|
|
122
122
|
"xregexp": "^2.0.0",
|
|
123
|
-
"boring": "^1.1.1",
|
|
124
123
|
"broadband": "^1.1.0",
|
|
125
|
-
"
|
|
126
|
-
"
|
|
124
|
+
"launder": "^1.7.1",
|
|
125
|
+
"@apostrophecms/db-connect": "^1.0.1",
|
|
127
126
|
"oembetter": "^1.2.0",
|
|
128
|
-
"
|
|
129
|
-
"
|
|
127
|
+
"boring": "^1.1.1",
|
|
128
|
+
"express-cache-on-demand": "^1.0.4",
|
|
129
|
+
"sanitize-html": "^2.17.5",
|
|
130
130
|
"uploadfs": "^1.26.1",
|
|
131
|
-
"
|
|
131
|
+
"postcss-viewport-to-container-toggle": "^2.3.0"
|
|
132
132
|
},
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"chai": "^4.3.10",
|
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
"mocha": "^11.7.5",
|
|
138
138
|
"nyc": "^17.1.0",
|
|
139
139
|
"stylelint": "^16.5.0",
|
|
140
|
-
"
|
|
141
|
-
"
|
|
140
|
+
"eslint-config-apostrophe": "^6.0.2",
|
|
141
|
+
"stylelint-config-apostrophe": "^4.4.0"
|
|
142
142
|
},
|
|
143
143
|
"browserslist": [
|
|
144
144
|
"ie >= 10"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "add-missing-schema-fields-project",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"../..": {
|
|
8
|
+
"version": "4.28.0",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@apostrophecms/emulate-mongo-3-driver": "workspace:^",
|
|
12
|
+
"@apostrophecms/vue-material-design-icons": "^1.0.0",
|
|
13
|
+
"@ctrl/tinycolor": "^4.1.0",
|
|
14
|
+
"@floating-ui/dom": "^1.5.3",
|
|
15
|
+
"@opentelemetry/api": "^1.9.0",
|
|
16
|
+
"@opentelemetry/semantic-conventions": "^1.0.1",
|
|
17
|
+
"@paralleldrive/cuid2": "^2.2.2",
|
|
18
|
+
"@tiptap/extension-color": "^2.4.0",
|
|
19
|
+
"@tiptap/extension-floating-menu": "^2.0.3",
|
|
20
|
+
"@tiptap/extension-highlight": "^2.0.3",
|
|
21
|
+
"@tiptap/extension-link": "^2.0.3",
|
|
22
|
+
"@tiptap/extension-placeholder": "^2.0.3",
|
|
23
|
+
"@tiptap/extension-subscript": "^2.0.3",
|
|
24
|
+
"@tiptap/extension-superscript": "^2.0.3",
|
|
25
|
+
"@tiptap/extension-table": "^2.0.3",
|
|
26
|
+
"@tiptap/extension-table-cell": "^2.0.3",
|
|
27
|
+
"@tiptap/extension-table-header": "^2.0.3",
|
|
28
|
+
"@tiptap/extension-table-row": "^2.0.3",
|
|
29
|
+
"@tiptap/extension-text-align": "^2.0.3",
|
|
30
|
+
"@tiptap/extension-text-style": "^2.0.3",
|
|
31
|
+
"@tiptap/extension-underline": "^2.0.3",
|
|
32
|
+
"@tiptap/starter-kit": "^2.0.3",
|
|
33
|
+
"@tiptap/vue-3": "^2.0.3",
|
|
34
|
+
"@vue/compiler-sfc": "^3.3.8",
|
|
35
|
+
"autoprefixer": "^10.4.1",
|
|
36
|
+
"bluebird": "^3.7.2",
|
|
37
|
+
"body-parser": "^1.18.2",
|
|
38
|
+
"boring": "workspace:^",
|
|
39
|
+
"broadband": "workspace:^",
|
|
40
|
+
"cheerio": "^1.0.0-rc.10",
|
|
41
|
+
"chokidar": "^3.5.2",
|
|
42
|
+
"common-tags": "^1.8.0",
|
|
43
|
+
"concat-with-sourcemaps": "^1.1.0",
|
|
44
|
+
"connect-mongo": "^5.1.0",
|
|
45
|
+
"cookie-parser": "^1.4.5",
|
|
46
|
+
"cors": "^2.8.5",
|
|
47
|
+
"css-loader": "^5.2.4",
|
|
48
|
+
"cssnano": "^7.1.1",
|
|
49
|
+
"csv-parse": "^5.6.0",
|
|
50
|
+
"dayjs": "^1.9.8",
|
|
51
|
+
"dompurify": "^3.2.5",
|
|
52
|
+
"encodeurl": "^2.0.0",
|
|
53
|
+
"express": "^4.16.4",
|
|
54
|
+
"express-bearer-token": "^3.0.0",
|
|
55
|
+
"express-cache-on-demand": "workspace:^",
|
|
56
|
+
"express-session": "^1.18.2",
|
|
57
|
+
"fs-extra": "^7.0.1",
|
|
58
|
+
"glob": "^10.4.5",
|
|
59
|
+
"he": "^1.2.0",
|
|
60
|
+
"html-to-text": "^9.0.5",
|
|
61
|
+
"i18next": "^20.3.2",
|
|
62
|
+
"i18next-http-middleware": "^3.1.5",
|
|
63
|
+
"import-fresh": "^3.3.0",
|
|
64
|
+
"is-wsl": "^2.2.0",
|
|
65
|
+
"jsdom": "^24.1.0",
|
|
66
|
+
"klona": "^2.0.4",
|
|
67
|
+
"launder": "^1.4.0",
|
|
68
|
+
"lodash": "^4.17.21",
|
|
69
|
+
"mini-css-extract-plugin": "^1.6.0",
|
|
70
|
+
"minimatch": "^3.0.4",
|
|
71
|
+
"mkdirp": "^0.5.5",
|
|
72
|
+
"multer": "^2.0.2",
|
|
73
|
+
"node-fetch": "^2.6.1",
|
|
74
|
+
"nodemailer": "^7.0.10",
|
|
75
|
+
"nunjucks": "^3.2.1",
|
|
76
|
+
"oembetter": "^1.1.3",
|
|
77
|
+
"parseurl": "^1.3.3",
|
|
78
|
+
"passport": "^0.6.0",
|
|
79
|
+
"passport-local": "^1.0.0",
|
|
80
|
+
"path-to-regexp": "^1.8.0",
|
|
81
|
+
"performance-now": "^2.1.0",
|
|
82
|
+
"pinia": "^2.1.7",
|
|
83
|
+
"postcss": "^8.4.47",
|
|
84
|
+
"postcss-html": "^1.3.0",
|
|
85
|
+
"postcss-loader": "^8.1.1",
|
|
86
|
+
"postcss-scss": "^4.0.3",
|
|
87
|
+
"postcss-viewport-to-container-toggle": "workspace:^",
|
|
88
|
+
"prompts": "^2.4.1",
|
|
89
|
+
"qs": "^6.10.1",
|
|
90
|
+
"regexp-quote": "0.0.0",
|
|
91
|
+
"resolve": "^1.19.0",
|
|
92
|
+
"resolve-from": "^5.0.0",
|
|
93
|
+
"sanitize-html": "workspace:^",
|
|
94
|
+
"sass": "^1.80.3",
|
|
95
|
+
"sass-loader": "^16.0.0",
|
|
96
|
+
"server-destroy": "^1.0.1",
|
|
97
|
+
"sluggo": "^1.0.0",
|
|
98
|
+
"sortablejs": "^1.15.0",
|
|
99
|
+
"sortablejs-vue3": "^1.2.11",
|
|
100
|
+
"tiny-emitter": "^2.1.0",
|
|
101
|
+
"tough-cookie": "^4.0.0",
|
|
102
|
+
"underscore.string": "^3.3.4",
|
|
103
|
+
"uploadfs": "workspace:^",
|
|
104
|
+
"void-elements": "^3.1.0",
|
|
105
|
+
"vue": "^3.5.20",
|
|
106
|
+
"vue-advanced-cropper": "^2.8.8",
|
|
107
|
+
"vue-loader": "^17.1.0",
|
|
108
|
+
"vue-style-loader": "^4.1.3",
|
|
109
|
+
"webpack": "^5.72.0",
|
|
110
|
+
"webpack-merge": "^5.7.3",
|
|
111
|
+
"xregexp": "^2.0.0"
|
|
112
|
+
},
|
|
113
|
+
"devDependencies": {
|
|
114
|
+
"eslint": "^9.39.1",
|
|
115
|
+
"eslint-config-apostrophe": "workspace:^",
|
|
116
|
+
"form-data": "^4.0.4",
|
|
117
|
+
"mocha": "^11.7.1",
|
|
118
|
+
"nyc": "^17.1.0",
|
|
119
|
+
"stylelint": "^16.5.0",
|
|
120
|
+
"stylelint-config-apostrophe": "workspace:^"
|
|
121
|
+
},
|
|
122
|
+
"engines": {
|
|
123
|
+
"node": ">=16.0.0"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"node_modules/apostrophe": {
|
|
127
|
+
"resolved": "../..",
|
|
128
|
+
"link": true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
package/test/files.js
CHANGED
|
@@ -132,6 +132,34 @@ describe('Files', function() {
|
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
+
it('should ignore a spoofed Host header when proxying the pretty URL (SSRF regression)', async function() {
|
|
136
|
+
const req = apos.task.getAnonReq();
|
|
137
|
+
try {
|
|
138
|
+
apos.file.options.prettyUrls = true;
|
|
139
|
+
const files = await apos.file.find(req).toArray();
|
|
140
|
+
assert.strictEqual(files.length, 1);
|
|
141
|
+
const file = files[0];
|
|
142
|
+
const attachment = apos.attachment.first(file);
|
|
143
|
+
const url = apos.attachment.url(attachment);
|
|
144
|
+
assert(url);
|
|
145
|
+
// Send an attacker-controlled Host header (e.g. the cloud metadata
|
|
146
|
+
// address from the advisory). The upstream fetch must be resolved
|
|
147
|
+
// against the server-trusted baseUrl, not this header, so the
|
|
148
|
+
// legitimate content is still served and the request is never
|
|
149
|
+
// steered at the spoofed host.
|
|
150
|
+
const response = await apos.http.get(url, {
|
|
151
|
+
headers: {
|
|
152
|
+
Host: '169.254.169.254'
|
|
153
|
+
},
|
|
154
|
+
fullResponse: true
|
|
155
|
+
});
|
|
156
|
+
assert.strictEqual(response.status, 200);
|
|
157
|
+
assert.strictEqual(response.body, attachment.data);
|
|
158
|
+
} finally {
|
|
159
|
+
apos.file.options.prettyUrls = false;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
135
163
|
});
|
|
136
164
|
|
|
137
165
|
describe('Files with i18n locale prefixes', function() {
|
package/test/utils.js
CHANGED
|
@@ -372,6 +372,109 @@ describe('Utils', async function() {
|
|
|
372
372
|
assert(data.shoes[0].size === 8);
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
+
// Server-Side Prototype Pollution (CWE-1321) regression coverage.
|
|
376
|
+
// apos.util.set and apos.util.get traverse user-supplied dot-notation
|
|
377
|
+
// paths. A segment of `__proto__`, `constructor` or `prototype` must
|
|
378
|
+
// never be followed, or an authenticated editor could write to
|
|
379
|
+
// Object.prototype (for example via the $pullAll patch operator) and
|
|
380
|
+
// poison authorization checks process-wide. See GHSA-6h5j-32cf-4253.
|
|
381
|
+
|
|
382
|
+
it('utils.set must reject a __proto__ segment instead of polluting Object.prototype', function() {
|
|
383
|
+
const data = {};
|
|
384
|
+
try {
|
|
385
|
+
assert.throws(() => {
|
|
386
|
+
apos.util.set(data, '__proto__.polluted', 'yes');
|
|
387
|
+
}, { name: 'invalid' });
|
|
388
|
+
assert.strictEqual({}.polluted, undefined);
|
|
389
|
+
assert.strictEqual(data.polluted, undefined);
|
|
390
|
+
} finally {
|
|
391
|
+
// Belt and suspenders: if the guard ever regresses, do not leak a
|
|
392
|
+
// polluted prototype into the rest of the suite.
|
|
393
|
+
delete Object.prototype.polluted;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('utils.set must reject a constructor.prototype segment', function() {
|
|
398
|
+
const data = {};
|
|
399
|
+
try {
|
|
400
|
+
assert.throws(() => {
|
|
401
|
+
apos.util.set(data, 'constructor.prototype.polluted', 'yes');
|
|
402
|
+
}, { name: 'invalid' });
|
|
403
|
+
assert.strictEqual({}.polluted, undefined);
|
|
404
|
+
} finally {
|
|
405
|
+
delete Object.prototype.polluted;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('utils.set must reject a trailing __proto__ segment rather than replacing the prototype', function() {
|
|
410
|
+
const data = {};
|
|
411
|
+
assert.throws(() => {
|
|
412
|
+
apos.util.set(data, '__proto__', { polluted: 'yes' });
|
|
413
|
+
}, { name: 'invalid' });
|
|
414
|
+
assert.strictEqual(Object.getPrototypeOf(data), Object.prototype);
|
|
415
|
+
assert.strictEqual(data.polluted, undefined);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('utils.set must reject a dangerous segment exposed after an @ reference', function() {
|
|
419
|
+
const data = {
|
|
420
|
+
items: [
|
|
421
|
+
{
|
|
422
|
+
_id: 'abc',
|
|
423
|
+
sub: {}
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
try {
|
|
428
|
+
assert.throws(() => {
|
|
429
|
+
apos.util.set(data, '@abc.__proto__.polluted', 'yes');
|
|
430
|
+
}, { name: 'invalid' });
|
|
431
|
+
assert.strictEqual({}.polluted, undefined);
|
|
432
|
+
} finally {
|
|
433
|
+
delete Object.prototype.polluted;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('utils.get must not traverse into the prototype chain via __proto__', function() {
|
|
438
|
+
// Without the guard this returns Object.prototype.toString (a function).
|
|
439
|
+
assert.strictEqual(apos.util.get({ a: 1 }, '__proto__.toString'), undefined);
|
|
440
|
+
assert.strictEqual(apos.util.get({ a: 1 }, '__proto__'), undefined);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('implementPatchOperators must not let a $pullAll key pollute Object.prototype', function() {
|
|
444
|
+
// The reported attack vector: an authenticated editor PATCH body of
|
|
445
|
+
// { $pullAll: { '__proto__.publicApiProjection': [] } } reached
|
|
446
|
+
// apos.util.set with a fully attacker-controlled key.
|
|
447
|
+
const patch = {
|
|
448
|
+
$pullAll: {
|
|
449
|
+
'__proto__.publicApiProjection': []
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
try {
|
|
453
|
+
assert.throws(() => {
|
|
454
|
+
apos.schema.implementPatchOperators(patch, {});
|
|
455
|
+
}, { name: 'invalid' });
|
|
456
|
+
assert.strictEqual({}.publicApiProjection, undefined);
|
|
457
|
+
} finally {
|
|
458
|
+
delete Object.prototype.publicApiProjection;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('implementPatchOperators must not let a direct dot-notation key pollute Object.prototype', function() {
|
|
463
|
+
// The second documented entry point: a top-level dotted key whose value
|
|
464
|
+
// is fully attacker-controlled.
|
|
465
|
+
const patch = {
|
|
466
|
+
'__proto__.publicApiProjection': { title: 1 }
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
assert.throws(() => {
|
|
470
|
+
apos.schema.implementPatchOperators(patch, {});
|
|
471
|
+
}, { name: 'invalid' });
|
|
472
|
+
assert.strictEqual({}.publicApiProjection, undefined);
|
|
473
|
+
} finally {
|
|
474
|
+
delete Object.prototype.publicApiProjection;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
375
478
|
it('should slugify', function () {
|
|
376
479
|
// Basic
|
|
377
480
|
assert.equal(
|