@appsemble/node-utils 0.36.2-test.5 → 0.36.3-test.5
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/README.md +3 -3
- package/authentication.js +5 -9
- package/package.json +10 -10
- package/readAsset.d.ts +1 -1
- package/server/routes/appRouter/indexHandler.js +3 -3
- package/server/routes/appRouter/securityHandler.d.ts +1 -1
- package/server/routes/appRouter/securityHandler.js +11 -2
- package/server/types.d.ts +1 -0
- package/server/utils/actions.js +43 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#  Appsemble Node Utilities
|
|
2
2
|
|
|
3
3
|
> NodeJS utilities used by Appsemble internally.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@appsemble/node-utils)
|
|
6
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.36.3-test.5)
|
|
7
7
|
[](https://prettier.io)
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
@@ -26,5 +26,5 @@ compatibility is not guaranteed.
|
|
|
26
26
|
|
|
27
27
|
## License
|
|
28
28
|
|
|
29
|
-
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.36.
|
|
29
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.36.3-test.5/LICENSE.md) ©
|
|
30
30
|
[Appsemble](https://appsemble.com)
|
package/authentication.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AppsembleError, logger } from '@appsemble/node-utils';
|
|
2
|
+
import { select } from '@inquirer/prompts';
|
|
2
3
|
import axios from 'axios';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
4
|
export const CREDENTIALS_ENV_VAR = 'APPSEMBLE_CLIENT_CREDENTIALS';
|
|
5
5
|
const authorizedRemotes = new Set();
|
|
6
6
|
export function getService(remote) {
|
|
@@ -34,14 +34,10 @@ async function getClientCredentials(remote, inputCredentials) {
|
|
|
34
34
|
[choice] = choices;
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
choices: choices.map((value) => ({ name: value.account, value })),
|
|
42
|
-
type: 'list',
|
|
43
|
-
},
|
|
44
|
-
]));
|
|
37
|
+
choice = await select({
|
|
38
|
+
message: 'Select client id to use',
|
|
39
|
+
choices: choices.map((value) => ({ name: value.account, value })),
|
|
40
|
+
});
|
|
45
41
|
}
|
|
46
42
|
return `${choice.account}:${choice.password}`;
|
|
47
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/node-utils",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.3-test.5",
|
|
4
4
|
"description": "NodeJS utilities used by Appsemble internally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -40,10 +40,9 @@
|
|
|
40
40
|
"test": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@appsemble/lang-sdk": "0.36.
|
|
44
|
-
"@appsemble/types": "0.36.
|
|
45
|
-
"@appsemble/utils": "0.36.
|
|
46
|
-
"@appsemble/webpack-core": "0.36.2-test.5",
|
|
43
|
+
"@appsemble/lang-sdk": "0.36.3-test.5",
|
|
44
|
+
"@appsemble/types": "0.36.3-test.5",
|
|
45
|
+
"@appsemble/utils": "0.36.3-test.5",
|
|
47
46
|
"@formatjs/fast-memoize": "^2.0.0",
|
|
48
47
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
49
48
|
"@kubernetes/client-node": "^0.22.2",
|
|
@@ -65,11 +64,11 @@
|
|
|
65
64
|
"fast-glob": "^3.0.0",
|
|
66
65
|
"form-data": "^4.0.4",
|
|
67
66
|
"fs-extra": "^11.0.0",
|
|
68
|
-
"inquirer": "^
|
|
67
|
+
"@inquirer/prompts": "^8.0.0",
|
|
69
68
|
"intl-messageformat": "^11.0.0",
|
|
70
69
|
"jsonschema": "~1.4.1",
|
|
71
70
|
"keytar": "^7.0.0",
|
|
72
|
-
"koa": "^
|
|
71
|
+
"koa": "^3.0.0",
|
|
73
72
|
"koa-compose": "^4.0.0",
|
|
74
73
|
"koa-mount": "^4.0.0",
|
|
75
74
|
"koa-static": "^5.0.0",
|
|
@@ -82,7 +81,7 @@
|
|
|
82
81
|
"lodash": "^4.0.0",
|
|
83
82
|
"lodash-es": "^4.0.0",
|
|
84
83
|
"logform": "^2.0.0",
|
|
85
|
-
"memfs": "
|
|
84
|
+
"memfs": "4.17.0",
|
|
86
85
|
"mime-types": "^2.0.0",
|
|
87
86
|
"minio": "^8.0.3",
|
|
88
87
|
"mustache": "^4.0.0",
|
|
@@ -90,6 +89,7 @@
|
|
|
90
89
|
"parse-duration": "^1.0.0",
|
|
91
90
|
"parse-json": "^8.0.0",
|
|
92
91
|
"pg": "^8.16.3",
|
|
92
|
+
"request-filtering-agent": "^3.2.0",
|
|
93
93
|
"sass": "^1.0.0",
|
|
94
94
|
"sharp": "^0.32.0",
|
|
95
95
|
"sort-keys": "^5.0.0",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"@types/css-tree": "2.3.11",
|
|
109
109
|
"@types/express-to-koa": "1.0.4",
|
|
110
110
|
"@types/koa-mount": "4.0.5",
|
|
111
|
-
"@types/mustache": "4.2.
|
|
111
|
+
"@types/mustache": "4.2.6",
|
|
112
112
|
"@types/type-is": "1.6.7",
|
|
113
113
|
"@types/uuid": "10.0.0",
|
|
114
114
|
"axios-mock-adapter": "1.22.0",
|
|
@@ -124,6 +124,6 @@
|
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
126
|
"engines": {
|
|
127
|
-
"node": ">=
|
|
127
|
+
"node": ">=24"
|
|
128
128
|
}
|
|
129
129
|
}
|
package/readAsset.d.ts
CHANGED
|
@@ -34,9 +34,9 @@ export function createIndexHandler({ createSettings, getApp, getAppDetails, getA
|
|
|
34
34
|
}
|
|
35
35
|
const appUrl = await getAppUrl({ app, context: ctx });
|
|
36
36
|
if (appUrl.hostname !== hostname) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
ctx.redirect(String(
|
|
37
|
+
const redirectUrl = new URL(path, appUrl);
|
|
38
|
+
redirectUrl.search = ctx.search;
|
|
39
|
+
ctx.redirect(String(redirectUrl));
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
const defaultLanguage = app.definition.defaultLanguage || defaultLocale;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { addDays } from 'date-fns';
|
|
2
2
|
import { assertKoaCondition } from '../../../index.js';
|
|
3
|
-
export function createSecurityHandler({ getApp }) {
|
|
3
|
+
export function createSecurityHandler({ getApp, getSecurityEmail }) {
|
|
4
4
|
return async (ctx) => {
|
|
5
5
|
const app = await getApp({ context: ctx, query: { attributes: ['id', 'updated'] } });
|
|
6
6
|
assertKoaCondition(app != null, ctx, 404, 'App not found');
|
|
7
7
|
const expires = addDays(new Date(app.$updated ?? app.$created), 180);
|
|
8
|
-
|
|
8
|
+
const securityEmail = getSecurityEmail();
|
|
9
|
+
ctx.type = 'text/plain; charset=utf-8';
|
|
10
|
+
ctx.body = [
|
|
11
|
+
`Contact: mailto:${securityEmail}`,
|
|
12
|
+
`Expires: ${expires.toISOString().replace(/\.\d{3}Z$/, 'Z')}`,
|
|
13
|
+
`Canonical: ${ctx.URL.origin}/.well-known/security.txt`,
|
|
14
|
+
'Policy: https://gitlab.com/appsemble/appsemble/-/blob/main/SECURITY.md',
|
|
15
|
+
'Preferred-Languages: en, nl',
|
|
16
|
+
'',
|
|
17
|
+
].join('\n');
|
|
9
18
|
};
|
|
10
19
|
}
|
|
11
20
|
//# sourceMappingURL=securityHandler.js.map
|
package/server/types.d.ts
CHANGED
|
@@ -373,6 +373,7 @@ export interface ParsedQuery {
|
|
|
373
373
|
}
|
|
374
374
|
export type ContentSecurityPolicy = Record<string, (string | false)[]>;
|
|
375
375
|
export interface Options {
|
|
376
|
+
getSecurityEmail: () => string;
|
|
376
377
|
getCurrentAppMember: (params: GetCurrentAppMemberParams) => Promise<AppMemberInfo | null>;
|
|
377
378
|
getCurrentAppMemberSelectedGroup: (params: GetCurrentAppMemberSelectedGroupParams) => Promise<AppMemberGroup | null>;
|
|
378
379
|
getCurrentAppMemberGroups: (params: GetCurrentAppMemberGroupsParams) => Promise<Group[] | null>;
|
package/server/utils/actions.js
CHANGED
|
@@ -2,6 +2,7 @@ import { defaultLocale, remap, } from '@appsemble/lang-sdk';
|
|
|
2
2
|
import { assertKoaCondition, createFormData, EmailQuotaExceededError, getContainerNamespace, getRemapperContext, logger, parseServiceUrl, scaleDeployment, setLastRequestAnnotation, throwKoaError, version, waitForPodReadiness, } from '@appsemble/node-utils';
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
import { get, mapValues, pick } from 'lodash-es';
|
|
5
|
+
import { RequestFilteringHttpAgent, RequestFilteringHttpsAgent } from 'request-filtering-agent';
|
|
5
6
|
/**
|
|
6
7
|
* These response headers are forwarded when proxying requests.
|
|
7
8
|
*/
|
|
@@ -121,6 +122,18 @@ async function handleRequestProxy(ctx, app, action, useBody, options) {
|
|
|
121
122
|
axiosConfig.decompress = false;
|
|
122
123
|
let response;
|
|
123
124
|
const containerUrlPattern = /^http:\/\/(([\da-z-]+).){2}svc.cluster.local/;
|
|
125
|
+
let isValidCompanionContainer = false;
|
|
126
|
+
// Block known Kubernetes internal hostnames that don't match the container pattern
|
|
127
|
+
const kubernetesInternalHostnames = [
|
|
128
|
+
'kubernetes',
|
|
129
|
+
'kubernetes.default',
|
|
130
|
+
'kubernetes.default.svc',
|
|
131
|
+
'kubernetes.default.svc.cluster.local',
|
|
132
|
+
];
|
|
133
|
+
if (kubernetesInternalHostnames.includes(proxyUrl.hostname.toLowerCase())) {
|
|
134
|
+
logger.warn(`SSRF blocked: Kubernetes internal hostname ${proxyUrl.hostname}`);
|
|
135
|
+
throwKoaError(ctx, 403, 'Access to Kubernetes internal services is not allowed');
|
|
136
|
+
}
|
|
124
137
|
// Restricting access to only the containers defined by the app
|
|
125
138
|
if (containerUrlPattern.test(String(proxyUrl))) {
|
|
126
139
|
axiosConfig.url = axiosConfig.url?.replace(axiosConfig.url.split('.')[1], getContainerNamespace());
|
|
@@ -128,6 +141,29 @@ async function handleRequestProxy(ctx, app, action, useBody, options) {
|
|
|
128
141
|
if (appId !== String(app.id)) {
|
|
129
142
|
throwKoaError(ctx, 403, 'Forbidden');
|
|
130
143
|
}
|
|
144
|
+
isValidCompanionContainer = true;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Apply SSRF protection for non-companion-container URLs
|
|
148
|
+
// This blocks requests to private IPs, localhost, link-local addresses,
|
|
149
|
+
// and hostnames that resolve to private IPs (prevents DNS rebinding attacks)
|
|
150
|
+
//
|
|
151
|
+
// VITEST_CONF_ALLOW_PRIVATE_IP_PROXY: Test-only env var set in vitest.setup.ts
|
|
152
|
+
// to allow proxy tests to use localhost servers. SSRF tests explicitly unset this.
|
|
153
|
+
// This env var should NEVER be set in production.
|
|
154
|
+
const allowPrivateIPAddress = process.env.VITEST_CONF_ALLOW_PRIVATE_IP_PROXY === '1';
|
|
155
|
+
// Preserve any existing agent options (e.g., client certs from applyAppServiceSecrets)
|
|
156
|
+
// while still applying SSRF protection
|
|
157
|
+
const existingHttpsOptions = axiosConfig.httpsAgent?.options ?? {};
|
|
158
|
+
const existingHttpOptions = axiosConfig.httpAgent?.options ?? {};
|
|
159
|
+
axiosConfig.httpAgent = new RequestFilteringHttpAgent({
|
|
160
|
+
...existingHttpOptions,
|
|
161
|
+
allowPrivateIPAddress,
|
|
162
|
+
});
|
|
163
|
+
axiosConfig.httpsAgent = new RequestFilteringHttpsAgent({
|
|
164
|
+
...existingHttpsOptions,
|
|
165
|
+
allowPrivateIPAddress,
|
|
166
|
+
});
|
|
131
167
|
}
|
|
132
168
|
logger.verbose(`Forwarding request to ${axios.getUri(axiosConfig)}`);
|
|
133
169
|
logger.verbose('Axios Config:');
|
|
@@ -137,9 +173,15 @@ async function handleRequestProxy(ctx, app, action, useBody, options) {
|
|
|
137
173
|
logger.verbose(response);
|
|
138
174
|
}
|
|
139
175
|
catch (err) {
|
|
176
|
+
// Check if this is an SSRF block from request-filtering-agent
|
|
177
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
178
|
+
if (errorMessage.includes('is not allowed') || errorMessage.includes('private')) {
|
|
179
|
+
logger.warn(`SSRF blocked: ${errorMessage}`);
|
|
180
|
+
throwKoaError(ctx, 403, 'Access to private network addresses is not allowed');
|
|
181
|
+
}
|
|
140
182
|
// If request is sent to a companion container and fails
|
|
141
183
|
// Try to start it anew and retry the request
|
|
142
|
-
if (
|
|
184
|
+
if (isValidCompanionContainer) {
|
|
143
185
|
const { deploymentName, namespace } = parseServiceUrl(String(proxyUrl));
|
|
144
186
|
try {
|
|
145
187
|
await scaleDeployment(namespace, deploymentName, 1);
|