@oneuptime/common 12.0.11 → 12.0.13
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/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
- package/Models/DatabaseModels/NetworkDevice.ts +64 -0
- package/Models/DatabaseModels/StatusPage.ts +59 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
- package/Server/API/AIChatAPI.ts +55 -56
- package/Server/API/CommonAPI.ts +89 -0
- package/Server/API/MicrosoftTeamsAPI.ts +20 -9
- package/Server/API/SlackAPI.ts +13 -10
- package/Server/API/StatusPageAPI.ts +2197 -2128
- package/Server/API/TelemetryAPI.ts +72 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/UserAuthorization.ts +11 -2
- package/Server/Services/ApiKeyPermissionService.ts +116 -2
- package/Server/Services/DatabaseService.ts +17 -6
- package/Server/Services/LogAggregationService.ts +34 -0
- package/Server/Services/LogService.ts +21 -0
- package/Server/Services/NetworkSiteService.ts +12 -0
- package/Server/Services/SpanService.ts +21 -0
- package/Server/Services/TraceAggregationService.ts +15 -0
- package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
- package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
- package/Server/Utils/APIKey/AccessPermission.ts +16 -40
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
- package/Server/Utils/LogRedaction.ts +576 -0
- package/Server/Utils/Logger.ts +123 -46
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
- package/Server/Utils/Monitor/MonitorResource.ts +29 -9
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
- package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
- package/Server/Utils/StartServer.ts +30 -9
- package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
- package/Server/Utils/VM/VMRunner.ts +693 -874
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
- package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
- package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
- package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
- package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
- package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
- package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
- package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
- package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
- package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
- package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
- package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
- package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
- package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
- package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
- package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
- package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
- package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
- package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
- package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
- package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
- package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
- package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
- package/Tests/UI/Components/MasterPage.test.tsx +36 -1
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
- package/Types/API/Hostname.ts +79 -0
- package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
- package/Types/Database/DatabaseProperty.ts +38 -0
- package/Types/Database/TableColumn.ts +34 -7
- package/Types/Events/Recurring.ts +16 -1
- package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
- package/Types/Monitor/MonitorStep.ts +13 -1
- package/Types/Monitor/MonitorType.ts +345 -40
- package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
- package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
- package/UI/Components/CardSelect/CardSelect.tsx +714 -105
- package/UI/Components/EmptyState/EmptyState.tsx +8 -1
- package/UI/Components/Footer/Footer.tsx +32 -6
- package/UI/Components/Forms/Fields/FormField.tsx +3 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Loader/PageLoader.tsx +29 -8
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
- package/UI/Components/MasterPage/MasterPage.tsx +24 -11
- package/Utils/Monitor/MonitorMetricType.ts +16 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
- package/Utils/Telemetry/CrossSignalScope.ts +75 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +48 -35
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +59 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +13 -10
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +745 -673
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +34 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +15 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +2 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogService.js +16 -0
- package/build/dist/Server/Services/LogService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +12 -0
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +16 -0
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +2 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +449 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +98 -45
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -7
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +63 -0
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/DatabaseProperty.js +38 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/TableColumn.js +24 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +13 -1
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +9 -1
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +330 -31
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
- package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
- package/build/dist/UI/Components/Footer/Footer.js +7 -4
- package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
- package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,361 +1,14 @@
|
|
|
1
1
|
import CapturedMetric from "../../../Types/Monitor/CustomCodeMonitor/CapturedMetric";
|
|
2
2
|
import ReturnResult from "../../../Types/IsolatedVM/ReturnResult";
|
|
3
|
-
import { JSONObject
|
|
3
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
4
4
|
import axios, { AxiosResponse } from "axios";
|
|
5
5
|
import crypto from "crypto";
|
|
6
6
|
import http from "http";
|
|
7
7
|
import https from "https";
|
|
8
8
|
import ivm from "isolated-vm";
|
|
9
9
|
import CaptureSpan from "../Telemetry/CaptureSpan";
|
|
10
|
-
import Dictionary from "../../../Types/Dictionary";
|
|
11
|
-
import GenericObject from "../../../Types/GenericObject";
|
|
12
|
-
import vm, { Context } from "vm";
|
|
13
10
|
import SSRFProtection from "../SSRFProtection";
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Symbol used to retrieve the real (unwrapped) target from a sandbox proxy.
|
|
17
|
-
* Hidden from user code via ownKeys / has traps.
|
|
18
|
-
*/
|
|
19
|
-
const PROXY_TARGET_SYMBOL: unique symbol = Symbol("sandboxProxyTarget");
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Hardening prelude injected before user code in `runCodeInNodeVM`.
|
|
23
|
-
*
|
|
24
|
-
* Node's `vm` module is not a security boundary. The published PoC for
|
|
25
|
-
* GHSA-g9cp-35m2-fjv6 forces a stack-overflow `RangeError`, walks
|
|
26
|
-
* `e.__proto__.__proto__.__proto__` to `Object.prototype`, then reads
|
|
27
|
-
* `.toString.constructor` to obtain a `Function` constructor that compiles
|
|
28
|
-
* code in a realm where `process.binding('spawn_sync')` is reachable.
|
|
29
|
-
*
|
|
30
|
-
* This prelude closes that path by:
|
|
31
|
-
* - severing `Error.prototype`'s link to `Object.prototype` so the 3-level
|
|
32
|
-
* walk lands on `null` instead of `Object.prototype`;
|
|
33
|
-
* - deleting `.constructor` from every built-in prototype, so even a
|
|
34
|
-
* different walk (e.g. `(0).constructor.constructor`) cannot resolve to a
|
|
35
|
-
* function constructor;
|
|
36
|
-
* - clearing `Function` / `eval` from the sandbox global;
|
|
37
|
-
* - freezing the affected prototypes so user code cannot reattach them.
|
|
38
|
-
*
|
|
39
|
-
* This is a hotfix for the public PoC. The durable fix is to drop
|
|
40
|
-
* `runCodeInNodeVM` in favor of running synthetic monitor scripts in an
|
|
41
|
-
* out-of-process sandbox (tracked on the `probe-runner` branch).
|
|
42
|
-
*/
|
|
43
|
-
const VM_HARDENING_PRELUDE: string = `(() => {
|
|
44
|
-
const _ctors = [
|
|
45
|
-
Object, Function, Array, String, Number, Boolean, RegExp,
|
|
46
|
-
Error, RangeError, TypeError, SyntaxError, ReferenceError, EvalError, URIError,
|
|
47
|
-
Symbol, Date, Map, Set, WeakMap, WeakSet, Promise, Proxy,
|
|
48
|
-
ArrayBuffer, DataView,
|
|
49
|
-
Int8Array, Uint8Array, Uint8ClampedArray,
|
|
50
|
-
Int16Array, Uint16Array, Int32Array, Uint32Array,
|
|
51
|
-
Float32Array, Float64Array,
|
|
52
|
-
];
|
|
53
|
-
if (typeof BigInt !== 'undefined') _ctors.push(BigInt);
|
|
54
|
-
|
|
55
|
-
for (const C of _ctors) {
|
|
56
|
-
try { if (C && C.prototype) delete C.prototype.constructor; } catch (_) {}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Generator / async-function prototypes have no named global — reach via syntax.
|
|
60
|
-
try { delete Object.getPrototypeOf(function*(){}).constructor; } catch (_) {}
|
|
61
|
-
try { delete Object.getPrototypeOf(async function(){}).constructor; } catch (_) {}
|
|
62
|
-
try { delete Object.getPrototypeOf(async function*(){}).constructor; } catch (_) {}
|
|
63
|
-
|
|
64
|
-
try { Object.setPrototypeOf(Error.prototype, null); } catch (_) {}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
Object.defineProperty(globalThis, 'Function', {
|
|
68
|
-
value: undefined, writable: false, configurable: false,
|
|
69
|
-
});
|
|
70
|
-
} catch (_) {}
|
|
71
|
-
try {
|
|
72
|
-
Object.defineProperty(globalThis, 'eval', {
|
|
73
|
-
value: undefined, writable: false, configurable: false,
|
|
74
|
-
});
|
|
75
|
-
} catch (_) {}
|
|
76
|
-
|
|
77
|
-
for (const C of _ctors) {
|
|
78
|
-
try { if (C && C.prototype) Object.freeze(C.prototype); } catch (_) {}
|
|
79
|
-
}
|
|
80
|
-
})();`;
|
|
81
|
-
|
|
82
|
-
/** Properties blocked on every host-realm object exposed to the sandbox. */
|
|
83
|
-
const BLOCKED_SANDBOX_PROPERTIES: ReadonlySet<string> = new Set([
|
|
84
|
-
"constructor",
|
|
85
|
-
"__proto__",
|
|
86
|
-
"prototype",
|
|
87
|
-
"mainModule",
|
|
88
|
-
/*
|
|
89
|
-
* Block Playwright methods that can spawn processes or access internals.
|
|
90
|
-
* Prevents RCE via browser.browserType().launch({executablePath:"/bin/sh"})
|
|
91
|
-
* and traversal via page.context().browser().browserType().launch(...)
|
|
92
|
-
*/
|
|
93
|
-
"browserType", // Browser → BrowserType (which has launch/connect)
|
|
94
|
-
"_browserType", // Internal alias for browserType — same escape vector
|
|
95
|
-
"launch", // BrowserType.launch() spawns a child process
|
|
96
|
-
"launchServer", // BrowserType.launchServer() spawns a browser server process
|
|
97
|
-
"launchPersistentContext", // BrowserType.launchPersistentContext() spawns a child process
|
|
98
|
-
"connectOverCDP", // BrowserType.connectOverCDP() connects via Chrome DevTools Protocol
|
|
99
|
-
"connect", // BrowserType.connect() connects to a remote browser
|
|
100
|
-
"newCDPSession", // BrowserContext/Page.newCDPSession() opens raw CDP sessions
|
|
101
|
-
]);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Wraps a host-realm value in a Proxy that blocks prototype-chain traversal.
|
|
105
|
-
* Primitives and null/undefined pass through unchanged.
|
|
106
|
-
* Object proxies are cached to preserve identity; function proxies are created
|
|
107
|
-
* per-access so they bind to the correct `this` (parent object).
|
|
108
|
-
*/
|
|
109
|
-
function createSandboxProxy(
|
|
110
|
-
value: unknown,
|
|
111
|
-
cache: WeakMap<GenericObject, unknown>,
|
|
112
|
-
parentObj?: GenericObject,
|
|
113
|
-
): unknown {
|
|
114
|
-
if (value === null || value === undefined) {
|
|
115
|
-
return value;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const valueType: string = typeof value;
|
|
119
|
-
|
|
120
|
-
if (valueType !== "object" && valueType !== "function") {
|
|
121
|
-
return value;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const target: GenericObject = value as GenericObject;
|
|
125
|
-
|
|
126
|
-
if (valueType === "function") {
|
|
127
|
-
/*
|
|
128
|
-
* Function proxies are NOT cached because the same function may be a method
|
|
129
|
-
* on different parent objects and needs a different `this` binding each time.
|
|
130
|
-
*/
|
|
131
|
-
const fnProxy: unknown = new Proxy(
|
|
132
|
-
target as (...args: unknown[]) => unknown,
|
|
133
|
-
{
|
|
134
|
-
get(
|
|
135
|
-
fnTarget: (...args: unknown[]) => unknown,
|
|
136
|
-
prop: string | symbol,
|
|
137
|
-
): unknown {
|
|
138
|
-
if (prop === PROXY_TARGET_SYMBOL) {
|
|
139
|
-
return fnTarget;
|
|
140
|
-
}
|
|
141
|
-
if (
|
|
142
|
-
typeof prop === "string" &&
|
|
143
|
-
BLOCKED_SANDBOX_PROPERTIES.has(prop)
|
|
144
|
-
) {
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
const val: unknown = Reflect.get(
|
|
148
|
-
fnTarget,
|
|
149
|
-
prop,
|
|
150
|
-
fnTarget as GenericObject,
|
|
151
|
-
);
|
|
152
|
-
return createSandboxProxy(val, cache, fnTarget as GenericObject);
|
|
153
|
-
},
|
|
154
|
-
getPrototypeOf(): null {
|
|
155
|
-
return null;
|
|
156
|
-
},
|
|
157
|
-
apply(
|
|
158
|
-
fnTarget: (...args: unknown[]) => unknown,
|
|
159
|
-
_thisArg: unknown,
|
|
160
|
-
args: unknown[],
|
|
161
|
-
): unknown {
|
|
162
|
-
const thisObj: GenericObject = (parentObj ||
|
|
163
|
-
fnTarget) as GenericObject;
|
|
164
|
-
try {
|
|
165
|
-
const result: unknown = Reflect.apply(fnTarget, thisObj, args);
|
|
166
|
-
if (result instanceof Promise) {
|
|
167
|
-
return result.then(
|
|
168
|
-
(v: unknown) => {
|
|
169
|
-
return createSandboxProxy(v, cache);
|
|
170
|
-
},
|
|
171
|
-
(err: unknown) => {
|
|
172
|
-
throw createSandboxProxy(err, cache);
|
|
173
|
-
},
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return createSandboxProxy(result, cache);
|
|
177
|
-
} catch (err: unknown) {
|
|
178
|
-
throw createSandboxProxy(err, cache);
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
has(
|
|
182
|
-
fnTarget: (...args: unknown[]) => unknown,
|
|
183
|
-
prop: string | symbol,
|
|
184
|
-
): boolean {
|
|
185
|
-
if (
|
|
186
|
-
typeof prop === "string" &&
|
|
187
|
-
BLOCKED_SANDBOX_PROPERTIES.has(prop)
|
|
188
|
-
) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
return Reflect.has(fnTarget, prop);
|
|
192
|
-
},
|
|
193
|
-
ownKeys(
|
|
194
|
-
fnTarget: (...args: unknown[]) => unknown,
|
|
195
|
-
): (string | symbol)[] {
|
|
196
|
-
return Reflect.ownKeys(fnTarget).filter((k: string | symbol) => {
|
|
197
|
-
return !(
|
|
198
|
-
typeof k === "string" && BLOCKED_SANDBOX_PROPERTIES.has(k)
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
},
|
|
202
|
-
getOwnPropertyDescriptor(
|
|
203
|
-
fnTarget: (...args: unknown[]) => unknown,
|
|
204
|
-
prop: string | symbol,
|
|
205
|
-
): PropertyDescriptor | undefined {
|
|
206
|
-
if (
|
|
207
|
-
typeof prop === "string" &&
|
|
208
|
-
BLOCKED_SANDBOX_PROPERTIES.has(prop)
|
|
209
|
-
) {
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
212
|
-
const desc: PropertyDescriptor | undefined =
|
|
213
|
-
Reflect.getOwnPropertyDescriptor(fnTarget, prop);
|
|
214
|
-
if (desc && "value" in desc) {
|
|
215
|
-
desc.value = createSandboxProxy(
|
|
216
|
-
desc.value,
|
|
217
|
-
cache,
|
|
218
|
-
fnTarget as GenericObject,
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
return desc;
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
);
|
|
225
|
-
return fnProxy;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Object — use cache to preserve identity and handle circular references
|
|
229
|
-
if (cache.has(target)) {
|
|
230
|
-
return cache.get(target);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const objProxy: GenericObject = new Proxy(target, {
|
|
234
|
-
get(objTarget: GenericObject, prop: string | symbol): unknown {
|
|
235
|
-
if (prop === PROXY_TARGET_SYMBOL) {
|
|
236
|
-
return objTarget;
|
|
237
|
-
}
|
|
238
|
-
if (typeof prop === "string" && BLOCKED_SANDBOX_PROPERTIES.has(prop)) {
|
|
239
|
-
return undefined;
|
|
240
|
-
}
|
|
241
|
-
const val: unknown = Reflect.get(objTarget, prop, objTarget);
|
|
242
|
-
return createSandboxProxy(val, cache, objTarget);
|
|
243
|
-
},
|
|
244
|
-
getPrototypeOf(): null {
|
|
245
|
-
return null;
|
|
246
|
-
},
|
|
247
|
-
set(
|
|
248
|
-
objTarget: GenericObject,
|
|
249
|
-
prop: string | symbol,
|
|
250
|
-
newValue: unknown,
|
|
251
|
-
): boolean {
|
|
252
|
-
return Reflect.set(objTarget, prop, newValue);
|
|
253
|
-
},
|
|
254
|
-
has(objTarget: GenericObject, prop: string | symbol): boolean {
|
|
255
|
-
if (typeof prop === "string" && BLOCKED_SANDBOX_PROPERTIES.has(prop)) {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
return Reflect.has(objTarget, prop);
|
|
259
|
-
},
|
|
260
|
-
ownKeys(objTarget: GenericObject): (string | symbol)[] {
|
|
261
|
-
return Reflect.ownKeys(objTarget).filter((k: string | symbol) => {
|
|
262
|
-
return !(typeof k === "string" && BLOCKED_SANDBOX_PROPERTIES.has(k));
|
|
263
|
-
});
|
|
264
|
-
},
|
|
265
|
-
getOwnPropertyDescriptor(
|
|
266
|
-
objTarget: GenericObject,
|
|
267
|
-
prop: string | symbol,
|
|
268
|
-
): PropertyDescriptor | undefined {
|
|
269
|
-
if (typeof prop === "string" && BLOCKED_SANDBOX_PROPERTIES.has(prop)) {
|
|
270
|
-
return undefined;
|
|
271
|
-
}
|
|
272
|
-
const desc: PropertyDescriptor | undefined =
|
|
273
|
-
Reflect.getOwnPropertyDescriptor(objTarget, prop);
|
|
274
|
-
if (desc && "value" in desc) {
|
|
275
|
-
desc.value = createSandboxProxy(desc.value, cache, objTarget);
|
|
276
|
-
}
|
|
277
|
-
return desc;
|
|
278
|
-
},
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
cache.set(target, objProxy);
|
|
282
|
-
return objProxy;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Recursively unwraps sandbox proxies in a return value so the host code
|
|
287
|
-
* receives original objects (e.g. Buffers that pass `instanceof` checks).
|
|
288
|
-
*/
|
|
289
|
-
export function deepUnwrapProxies(
|
|
290
|
-
value: unknown,
|
|
291
|
-
visited?: WeakSet<GenericObject>,
|
|
292
|
-
): unknown {
|
|
293
|
-
if (value === null || value === undefined) {
|
|
294
|
-
return value;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const valueType: string = typeof value;
|
|
298
|
-
|
|
299
|
-
if (valueType !== "object" && valueType !== "function") {
|
|
300
|
-
return value;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const obj: Record<string | symbol, unknown> = value as Record<
|
|
304
|
-
string | symbol,
|
|
305
|
-
unknown
|
|
306
|
-
>;
|
|
307
|
-
|
|
308
|
-
// If it's one of our proxies, unwrap to the original target
|
|
309
|
-
try {
|
|
310
|
-
const underlying: unknown = obj[PROXY_TARGET_SYMBOL];
|
|
311
|
-
if (underlying !== undefined) {
|
|
312
|
-
return underlying;
|
|
313
|
-
}
|
|
314
|
-
} catch {
|
|
315
|
-
// Not a proxy or symbol access failed — treat as a plain value
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (!visited) {
|
|
319
|
-
visited = new WeakSet<GenericObject>();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (visited.has(obj as GenericObject)) {
|
|
323
|
-
return obj;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
visited.add(obj as GenericObject);
|
|
327
|
-
|
|
328
|
-
if (Array.isArray(obj)) {
|
|
329
|
-
for (let i: number = 0; i < obj.length; i++) {
|
|
330
|
-
(obj as unknown[])[i] = deepUnwrapProxies((obj as unknown[])[i], visited);
|
|
331
|
-
}
|
|
332
|
-
} else if (valueType === "object") {
|
|
333
|
-
for (const key of Object.keys(obj as Record<string, unknown>)) {
|
|
334
|
-
(obj as Record<string, unknown>)[key] = deepUnwrapProxies(
|
|
335
|
-
(obj as Record<string, unknown>)[key],
|
|
336
|
-
visited,
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return obj;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Unwraps a single value if it is a sandbox proxy, otherwise returns it as-is.
|
|
346
|
-
*/
|
|
347
|
-
function unwrapProxy<T>(value: T): T {
|
|
348
|
-
if (value && typeof value === "object") {
|
|
349
|
-
const underlying: unknown = (value as Record<symbol, unknown>)[
|
|
350
|
-
PROXY_TARGET_SYMBOL
|
|
351
|
-
];
|
|
352
|
-
if (underlying !== undefined) {
|
|
353
|
-
return underlying as T;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return value;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
12
|
export default class VMRunner {
|
|
360
13
|
/*
|
|
361
14
|
* Works out which URL the sandbox's axios call will actually dial, so the
|
|
@@ -403,250 +56,100 @@ export default class VMRunner {
|
|
|
403
56
|
}
|
|
404
57
|
|
|
405
58
|
@CaptureSpan()
|
|
406
|
-
public static async
|
|
59
|
+
public static async runCodeInSandbox(data: {
|
|
407
60
|
code: string;
|
|
408
61
|
options: {
|
|
409
62
|
timeout?: number;
|
|
410
63
|
args?: JSONObject | undefined;
|
|
411
|
-
context?: Dictionary<GenericObject | string> | undefined;
|
|
412
64
|
};
|
|
413
65
|
}): Promise<ReturnResult> {
|
|
414
66
|
const { code, options } = data;
|
|
415
67
|
const timeout: number = options.timeout || 5000;
|
|
416
68
|
|
|
417
69
|
const logMessages: string[] = [];
|
|
418
|
-
const MAX_LOG_BYTES: number = 1_000_000; // 1MB cap
|
|
419
|
-
let totalLogBytes: number = 0;
|
|
420
|
-
|
|
421
70
|
const capturedMetrics: CapturedMetric[] = [];
|
|
422
71
|
const MAX_METRICS: number = 100;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
72
|
+
const MAX_LOG_MESSAGES: number = 1000;
|
|
73
|
+
const MAX_LOG_BYTES: number = 1_000_000;
|
|
74
|
+
const MAX_SCRIPT_ERROR_MESSAGE_LENGTH: number = 10_000;
|
|
75
|
+
let logBytes: number = 0;
|
|
76
|
+
|
|
77
|
+
const pendingHostTimeouts: Set<ReturnType<typeof global.setTimeout>> =
|
|
78
|
+
new Set<ReturnType<typeof global.setTimeout>>();
|
|
79
|
+
const pendingAxiosControllers: Set<AbortController> =
|
|
80
|
+
new Set<AbortController>();
|
|
81
|
+
let acceptingHostOperations: boolean = true;
|
|
82
|
+
type PendingAxiosOperation =
|
|
83
|
+
| { status: "pending" }
|
|
84
|
+
| { status: "fulfilled"; value: string }
|
|
85
|
+
| { status: "rejected"; errorMessage: string };
|
|
86
|
+
const pendingAxiosOperations: Map<string, PendingAxiosOperation> = new Map<
|
|
87
|
+
string,
|
|
88
|
+
PendingAxiosOperation
|
|
89
|
+
>();
|
|
90
|
+
let nextAxiosOperationId: number = 0;
|
|
91
|
+
|
|
92
|
+
type PendingSleepOperation = {
|
|
93
|
+
settled: boolean;
|
|
441
94
|
};
|
|
95
|
+
const pendingSleepOperations: Map<string, PendingSleepOperation> = new Map<
|
|
96
|
+
string,
|
|
97
|
+
PendingSleepOperation
|
|
98
|
+
>();
|
|
99
|
+
let nextSleepOperationId: number = 0;
|
|
442
100
|
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
):
|
|
446
|
-
|
|
447
|
-
clearTimeout(actual);
|
|
448
|
-
const idx: number = pendingTimeouts.indexOf(actual);
|
|
449
|
-
if (idx !== -1) {
|
|
450
|
-
pendingTimeouts.splice(idx, 1);
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
const wrappedSetInterval: (
|
|
455
|
-
fn: (...args: unknown[]) => void,
|
|
456
|
-
ms?: number,
|
|
457
|
-
...rest: unknown[]
|
|
458
|
-
) => TimerHandle = (
|
|
459
|
-
fn: (...args: unknown[]) => void,
|
|
460
|
-
ms?: number,
|
|
461
|
-
...rest: unknown[]
|
|
462
|
-
): TimerHandle => {
|
|
463
|
-
const handle: TimerHandle = setInterval(fn, ms, ...rest);
|
|
464
|
-
pendingIntervals.push(handle);
|
|
465
|
-
return handle;
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
const wrappedClearInterval: (handle: TimerHandle) => void = (
|
|
469
|
-
handle: TimerHandle,
|
|
470
|
-
): void => {
|
|
471
|
-
const actual: TimerHandle = unwrapProxy(handle);
|
|
472
|
-
clearInterval(actual);
|
|
473
|
-
const idx: number = pendingIntervals.indexOf(actual);
|
|
474
|
-
if (idx !== -1) {
|
|
475
|
-
pendingIntervals.splice(idx, 1);
|
|
476
|
-
}
|
|
477
|
-
};
|
|
101
|
+
const sanitizeScriptError: (error: unknown) => Error = (
|
|
102
|
+
error: unknown,
|
|
103
|
+
): Error => {
|
|
104
|
+
let message: string = "Sandbox script failed";
|
|
478
105
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
totalLogBytes += msg.length;
|
|
490
|
-
if (totalLogBytes <= MAX_LOG_BYTES) {
|
|
491
|
-
logMessages.push(msg);
|
|
492
|
-
}
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
proxyCache,
|
|
496
|
-
);
|
|
497
|
-
sandbox["http"] = createSandboxProxy(http, proxyCache);
|
|
498
|
-
sandbox["https"] = createSandboxProxy(https, proxyCache);
|
|
499
|
-
sandbox["axios"] = createSandboxProxy(axios, proxyCache);
|
|
500
|
-
sandbox["crypto"] = createSandboxProxy(crypto, proxyCache);
|
|
501
|
-
sandbox["setTimeout"] = createSandboxProxy(wrappedSetTimeout, proxyCache);
|
|
502
|
-
sandbox["clearTimeout"] = createSandboxProxy(
|
|
503
|
-
wrappedClearTimeout,
|
|
504
|
-
proxyCache,
|
|
505
|
-
);
|
|
506
|
-
sandbox["setInterval"] = createSandboxProxy(wrappedSetInterval, proxyCache);
|
|
507
|
-
sandbox["clearInterval"] = createSandboxProxy(
|
|
508
|
-
wrappedClearInterval,
|
|
509
|
-
proxyCache,
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
sandbox["oneuptime"] = createSandboxProxy(
|
|
513
|
-
{
|
|
514
|
-
captureMetric: (
|
|
515
|
-
name: unknown,
|
|
516
|
-
value: unknown,
|
|
517
|
-
attributes?: unknown,
|
|
518
|
-
): void => {
|
|
519
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (typeof value !== "number" || isNaN(value)) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
if (capturedMetrics.length >= MAX_METRICS) {
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
const metric: CapturedMetric = {
|
|
529
|
-
name: name.substring(0, 200),
|
|
530
|
-
value: value,
|
|
531
|
-
};
|
|
532
|
-
if (attributes && typeof attributes === "object") {
|
|
533
|
-
const safeAttrs: JSONObject = {};
|
|
534
|
-
for (const [k, v] of Object.entries(
|
|
535
|
-
attributes as Record<string, unknown>,
|
|
536
|
-
)) {
|
|
537
|
-
if (
|
|
538
|
-
typeof v === "string" ||
|
|
539
|
-
typeof v === "number" ||
|
|
540
|
-
typeof v === "boolean"
|
|
541
|
-
) {
|
|
542
|
-
safeAttrs[k] = String(v);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
metric.attributes = safeAttrs;
|
|
106
|
+
try {
|
|
107
|
+
if (typeof error === "string") {
|
|
108
|
+
message = error;
|
|
109
|
+
} else if (error && typeof error === "object") {
|
|
110
|
+
const candidateMessage: unknown = (error as { message?: unknown })[
|
|
111
|
+
"message"
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
if (typeof candidateMessage === "string") {
|
|
115
|
+
message = candidateMessage;
|
|
546
116
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
proxyCache,
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
// Wrap any additional context (e.g. Playwright browser/page objects)
|
|
554
|
-
if (options.context) {
|
|
555
|
-
for (const key of Object.keys(options.context)) {
|
|
556
|
-
const val: GenericObject | string | undefined = options.context[key];
|
|
557
|
-
sandbox[key] =
|
|
558
|
-
typeof val === "string" ? val : createSandboxProxy(val, proxyCache);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Do not invoke any attacker-controlled coercion while reporting errors.
|
|
559
120
|
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (options.args) {
|
|
563
|
-
// args is plain JSON data — no host functions to protect against
|
|
564
|
-
sandbox["args"] = options.args;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
vm.createContext(sandbox, {
|
|
568
|
-
codeGeneration: {
|
|
569
|
-
strings: false,
|
|
570
|
-
wasm: false,
|
|
571
|
-
},
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
const script: string = `(async()=>{
|
|
575
|
-
${VM_HARDENING_PRELUDE}
|
|
576
|
-
${code}
|
|
577
|
-
})()`;
|
|
578
121
|
|
|
579
|
-
|
|
580
|
-
let returnVal: unknown;
|
|
581
|
-
let scriptError: Error | undefined;
|
|
122
|
+
let sanitizedMessage: string = "";
|
|
582
123
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
* vm timeout only covers synchronous CPU time, so wrap with
|
|
586
|
-
* Promise.race to also cover async operations (network, timers, etc.)
|
|
587
|
-
*/
|
|
588
|
-
const vmPromise: Promise<unknown> = vm.runInContext(script, sandbox, {
|
|
589
|
-
timeout: timeout,
|
|
590
|
-
});
|
|
124
|
+
for (const character of message) {
|
|
125
|
+
const characterCode: number = character.charCodeAt(0);
|
|
591
126
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
// Don't let this timer keep the process alive
|
|
601
|
-
handle.unref();
|
|
602
|
-
},
|
|
603
|
-
);
|
|
127
|
+
if (
|
|
128
|
+
characterCode === 9 ||
|
|
129
|
+
characterCode === 10 ||
|
|
130
|
+
characterCode === 13 ||
|
|
131
|
+
(characterCode >= 32 && characterCode !== 127)
|
|
132
|
+
) {
|
|
133
|
+
sanitizedMessage += character;
|
|
134
|
+
}
|
|
604
135
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
* Capture user-thrown errors (including timeouts) so the caller can
|
|
609
|
-
* still access side-channel data collected before the throw — e.g.
|
|
610
|
-
* screenshots assigned to a host-realm object passed via `context`.
|
|
611
|
-
* Rethrowing here would discard those partial results.
|
|
612
|
-
*/
|
|
613
|
-
scriptError =
|
|
614
|
-
err instanceof Error
|
|
615
|
-
? err
|
|
616
|
-
: new Error(typeof err === "string" ? err : String(err));
|
|
136
|
+
if (sanitizedMessage.length >= MAX_SCRIPT_ERROR_MESSAGE_LENGTH) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
617
139
|
}
|
|
618
140
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
scriptError,
|
|
624
|
-
};
|
|
625
|
-
} finally {
|
|
626
|
-
// Clean up any lingering timers to prevent resource leaks
|
|
627
|
-
for (const handle of pendingTimeouts) {
|
|
628
|
-
clearTimeout(handle);
|
|
629
|
-
}
|
|
630
|
-
for (const handle of pendingIntervals) {
|
|
631
|
-
clearInterval(handle);
|
|
141
|
+
message = sanitizedMessage.substring(0, MAX_SCRIPT_ERROR_MESSAGE_LENGTH);
|
|
142
|
+
|
|
143
|
+
if (!message) {
|
|
144
|
+
message = "Sandbox script failed";
|
|
632
145
|
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
146
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
args?: JSONObject | undefined;
|
|
147
|
+
/*
|
|
148
|
+
* Deliberately create a fresh host Error so isolate-owned properties and
|
|
149
|
+
* stack frames never escape with the result.
|
|
150
|
+
*/
|
|
151
|
+
return new Error(message);
|
|
642
152
|
};
|
|
643
|
-
}): Promise<ReturnResult> {
|
|
644
|
-
const { code, options } = data;
|
|
645
|
-
const timeout: number = options.timeout || 5000;
|
|
646
|
-
|
|
647
|
-
const logMessages: string[] = [];
|
|
648
|
-
const capturedMetrics: CapturedMetric[] = [];
|
|
649
|
-
const MAX_METRICS: number = 100;
|
|
650
153
|
|
|
651
154
|
const isolate: ivm.Isolate = new ivm.Isolate({ memoryLimit: 128 });
|
|
652
155
|
|
|
@@ -657,24 +160,73 @@ export default class VMRunner {
|
|
|
657
160
|
// Set up global object
|
|
658
161
|
await jail.set("global", jail.derefInto());
|
|
659
162
|
|
|
660
|
-
|
|
163
|
+
/*
|
|
164
|
+
* Callback values become ordinary functions in the destination isolate.
|
|
165
|
+
* Never expose ivm.Reference or ivm.ExternalCopy handles to user code:
|
|
166
|
+
* their prototype methods can be used to cross the isolate boundary.
|
|
167
|
+
*/
|
|
661
168
|
await jail.set(
|
|
662
|
-
"
|
|
663
|
-
new ivm.Callback(
|
|
664
|
-
|
|
665
|
-
|
|
169
|
+
"__oneuptimeHostLogCallback",
|
|
170
|
+
new ivm.Callback(
|
|
171
|
+
(message: string) => {
|
|
172
|
+
if (logMessages.length >= MAX_LOG_MESSAGES) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const messageBytes: number = Buffer.byteLength(message, "utf8");
|
|
177
|
+
|
|
178
|
+
if (logBytes + messageBytes > MAX_LOG_BYTES) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
logBytes += messageBytes;
|
|
183
|
+
logMessages.push(message);
|
|
184
|
+
},
|
|
185
|
+
{ sync: true },
|
|
186
|
+
),
|
|
666
187
|
);
|
|
667
188
|
|
|
668
189
|
await context.eval(`
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
190
|
+
(() => {
|
|
191
|
+
const hostLog = globalThis.__oneuptimeHostLogCallback;
|
|
192
|
+
delete globalThis.__oneuptimeHostLogCallback;
|
|
193
|
+
let sandboxLogCount = 0;
|
|
194
|
+
let sandboxLogCharacters = 0;
|
|
195
|
+
|
|
196
|
+
const sandboxConsole = Object.freeze({
|
|
197
|
+
log: (...args) => {
|
|
198
|
+
if (sandboxLogCount >= 1000 || sandboxLogCharacters >= 500000) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const message = args.map(value => {
|
|
203
|
+
try {
|
|
204
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
205
|
+
} catch (_) {
|
|
206
|
+
return String(value);
|
|
207
|
+
}
|
|
208
|
+
}).join(' ').substring(0, 250000);
|
|
209
|
+
|
|
210
|
+
if (sandboxLogCharacters + message.length > 500000) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
sandboxLogCount += 1;
|
|
215
|
+
sandboxLogCharacters += message.length;
|
|
216
|
+
hostLog(message);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
Object.defineProperty(globalThis, 'console', {
|
|
221
|
+
value: sandboxConsole,
|
|
222
|
+
writable: false,
|
|
223
|
+
configurable: false,
|
|
224
|
+
});
|
|
225
|
+
})();
|
|
673
226
|
`);
|
|
674
227
|
|
|
675
|
-
// oneuptime.captureMetric - fire-and-forget callback
|
|
676
228
|
await jail.set(
|
|
677
|
-
"
|
|
229
|
+
"__oneuptimeHostMetricCallback",
|
|
678
230
|
new ivm.Callback(
|
|
679
231
|
(name: string, value: string, attributesJson?: string) => {
|
|
680
232
|
if (capturedMetrics.length >= MAX_METRICS) {
|
|
@@ -697,27 +249,47 @@ export default class VMRunner {
|
|
|
697
249
|
}
|
|
698
250
|
capturedMetrics.push(metric);
|
|
699
251
|
},
|
|
252
|
+
{ sync: true },
|
|
700
253
|
),
|
|
701
254
|
);
|
|
702
255
|
|
|
703
256
|
await context.eval(`
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
257
|
+
(() => {
|
|
258
|
+
const hostCaptureMetric = globalThis.__oneuptimeHostMetricCallback;
|
|
259
|
+
delete globalThis.__oneuptimeHostMetricCallback;
|
|
260
|
+
|
|
261
|
+
const sandboxOneUptime = Object.freeze({
|
|
262
|
+
captureMetric: (name, value, attributes) => {
|
|
263
|
+
if (typeof name !== 'string' || name.length === 0) return;
|
|
264
|
+
if (typeof value !== 'number' || isNaN(value)) return;
|
|
265
|
+
const attrJson = attributes ? JSON.stringify(attributes) : undefined;
|
|
266
|
+
hostCaptureMetric(String(name), String(value), attrJson);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
Object.defineProperty(globalThis, 'oneuptime', {
|
|
271
|
+
value: sandboxOneUptime,
|
|
272
|
+
writable: false,
|
|
273
|
+
configurable: false,
|
|
274
|
+
});
|
|
275
|
+
})();
|
|
712
276
|
`);
|
|
713
277
|
|
|
714
278
|
// args - deep copy into isolate
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
279
|
+
await jail.set("__oneuptimeCopiedArgs", options.args || {}, {
|
|
280
|
+
copy: true,
|
|
281
|
+
});
|
|
282
|
+
await context.eval(`
|
|
283
|
+
(() => {
|
|
284
|
+
const copiedArgs = globalThis.__oneuptimeCopiedArgs;
|
|
285
|
+
delete globalThis.__oneuptimeCopiedArgs;
|
|
286
|
+
Object.defineProperty(globalThis, 'args', {
|
|
287
|
+
value: copiedArgs,
|
|
288
|
+
writable: false,
|
|
289
|
+
configurable: false,
|
|
290
|
+
});
|
|
291
|
+
})();
|
|
292
|
+
`);
|
|
721
293
|
|
|
722
294
|
/*
|
|
723
295
|
* http / https - provide Agent constructors that serialize across the boundary.
|
|
@@ -745,212 +317,317 @@ export default class VMRunner {
|
|
|
745
317
|
|
|
746
318
|
/*
|
|
747
319
|
* axios (get, head, options, post, put, patch, delete, request)
|
|
748
|
-
* bridged
|
|
320
|
+
* bridged through a copied async callback.
|
|
749
321
|
*
|
|
750
322
|
* For GET/HEAD/OPTIONS/DELETE: args = [method, url, configJson?]
|
|
751
323
|
* For POST/PUT/PATCH: args = [method, url, bodyJson?, configJson?]
|
|
752
324
|
* For REQUEST: args = ['request', '', configJson]
|
|
753
325
|
*/
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const hasBody: boolean = methodsWithBody.includes(method);
|
|
326
|
+
const executeAxiosRequest: (
|
|
327
|
+
signal: AbortSignal,
|
|
328
|
+
method: string,
|
|
329
|
+
url: string,
|
|
330
|
+
arg1?: string,
|
|
331
|
+
arg2?: string,
|
|
332
|
+
) => Promise<string> = async (
|
|
333
|
+
signal: AbortSignal,
|
|
334
|
+
method: string,
|
|
335
|
+
url: string,
|
|
336
|
+
arg1?: string,
|
|
337
|
+
arg2?: string,
|
|
338
|
+
): Promise<string> => {
|
|
339
|
+
const methodsWithBody: string[] = ["post", "put", "patch"];
|
|
340
|
+
const hasBody: boolean = methodsWithBody.includes(method);
|
|
770
341
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const configStr: string | undefined = hasBody ? arg2 : arg1;
|
|
779
|
-
let config: JSONObject | undefined = configStr
|
|
780
|
-
? (JSON.parse(configStr) as JSONObject)
|
|
781
|
-
: undefined;
|
|
782
|
-
|
|
783
|
-
// Reconstruct real http/https Agents from serialized markers
|
|
784
|
-
if (config) {
|
|
785
|
-
const httpsAgentConfig: JSONObject | undefined = config[
|
|
786
|
-
"httpsAgent"
|
|
787
|
-
] as JSONObject | undefined;
|
|
788
|
-
|
|
789
|
-
if (
|
|
790
|
-
httpsAgentConfig &&
|
|
791
|
-
httpsAgentConfig["__agentType"] === "__https_agent__"
|
|
792
|
-
) {
|
|
793
|
-
config["httpsAgent"] = new https.Agent(
|
|
794
|
-
httpsAgentConfig["options"] as https.AgentOptions,
|
|
795
|
-
) as unknown as JSONObject;
|
|
796
|
-
}
|
|
342
|
+
/*
|
|
343
|
+
* For POST/PUT/PATCH: arg1=body, arg2=config
|
|
344
|
+
* For GET/HEAD/OPTIONS/DELETE/REQUEST: arg1=config
|
|
345
|
+
*/
|
|
346
|
+
const body: JSONObject | undefined =
|
|
347
|
+
hasBody && arg1 ? (JSON.parse(arg1) as JSONObject) : undefined;
|
|
797
348
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
349
|
+
const configStr: string | undefined = hasBody ? arg2 : arg1;
|
|
350
|
+
let config: JSONObject | undefined = configStr
|
|
351
|
+
? (JSON.parse(configStr) as JSONObject)
|
|
352
|
+
: undefined;
|
|
353
|
+
|
|
354
|
+
// Reconstruct real http/https Agents from serialized markers
|
|
355
|
+
if (config) {
|
|
356
|
+
const httpsAgentConfig: JSONObject | undefined = config[
|
|
357
|
+
"httpsAgent"
|
|
358
|
+
] as JSONObject | undefined;
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
httpsAgentConfig &&
|
|
362
|
+
httpsAgentConfig["__agentType"] === "__https_agent__"
|
|
363
|
+
) {
|
|
364
|
+
config["httpsAgent"] = new https.Agent(
|
|
365
|
+
httpsAgentConfig["options"] as https.AgentOptions,
|
|
366
|
+
) as unknown as JSONObject;
|
|
810
367
|
}
|
|
811
368
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
369
|
+
const httpAgentConfig: JSONObject | undefined = config[
|
|
370
|
+
"httpAgent"
|
|
371
|
+
] as JSONObject | undefined;
|
|
372
|
+
|
|
373
|
+
if (
|
|
374
|
+
httpAgentConfig &&
|
|
375
|
+
httpAgentConfig["__agentType"] === "__http_agent__"
|
|
376
|
+
) {
|
|
377
|
+
config["httpAgent"] = new http.Agent(
|
|
378
|
+
httpAgentConfig["options"] as http.AgentOptions,
|
|
379
|
+
) as unknown as JSONObject;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Helper: convert AxiosHeaders (or any header-like object) to a
|
|
385
|
+
* plain record so it can be safely JSON-serialised.
|
|
386
|
+
*/
|
|
387
|
+
const toPlainHeaders: (headers: unknown) => Record<string, unknown> = (
|
|
388
|
+
headers: unknown,
|
|
389
|
+
): Record<string, unknown> => {
|
|
390
|
+
const plain: Record<string, unknown> = {};
|
|
391
|
+
if (headers) {
|
|
392
|
+
for (const hKey of Object.keys(
|
|
393
|
+
headers as Record<string, unknown>,
|
|
394
|
+
)) {
|
|
395
|
+
plain[hKey] = (headers as Record<string, unknown>)[hKey];
|
|
828
396
|
}
|
|
829
|
-
|
|
830
|
-
|
|
397
|
+
}
|
|
398
|
+
return plain;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/*
|
|
402
|
+
* SSRF guard (GHSA-v5xh-rw9h-77fv).
|
|
403
|
+
*
|
|
404
|
+
* This bridge hands the host process's real axios to code the user
|
|
405
|
+
* wrote - the Custom JavaScript workflow component documents "you can
|
|
406
|
+
* use axios module" - so without a check here it is a strictly more
|
|
407
|
+
* capable version of the hole that was reported against the API
|
|
408
|
+
* components: arbitrary method, headers and body against the internal
|
|
409
|
+
* network, with the full response marshalled back into the sandbox
|
|
410
|
+
* and on into the workflow log.
|
|
411
|
+
*
|
|
412
|
+
* It has to live on THIS side of the isolate boundary. The
|
|
413
|
+
* sandbox-side axios shim is attacker-editable - user code can simply
|
|
414
|
+
* redefine it - so a check there guards nothing.
|
|
415
|
+
*/
|
|
416
|
+
const effectiveUrl: string = VMRunner.resolveEffectiveRequestUrl({
|
|
417
|
+
method,
|
|
418
|
+
url,
|
|
419
|
+
config,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await SSRFProtection.validateWebhookTargetIsSafe(effectiveUrl);
|
|
423
|
+
|
|
424
|
+
/*
|
|
425
|
+
* The URL that was just validated has to be the URL that gets
|
|
426
|
+
* dialled. Each of these would otherwise steer the connection
|
|
427
|
+
* somewhere else entirely, past the check above:
|
|
428
|
+
* proxy - sends the request to an arbitrary host:port
|
|
429
|
+
* socketPath - connects to a unix socket (/var/run/docker.sock)
|
|
430
|
+
* transport / adapter - replaces the transport wholesale
|
|
431
|
+
* and a redirect would let a validated public host nominate an
|
|
432
|
+
* internal one on the second hop.
|
|
433
|
+
*/
|
|
434
|
+
const safeConfig: JSONObject = config || {};
|
|
435
|
+
delete safeConfig["proxy"];
|
|
436
|
+
delete safeConfig["socketPath"];
|
|
437
|
+
delete safeConfig["transport"];
|
|
438
|
+
delete safeConfig["adapter"];
|
|
439
|
+
safeConfig["maxRedirects"] = 0;
|
|
440
|
+
Object.defineProperty(safeConfig, "signal", {
|
|
441
|
+
value: signal,
|
|
442
|
+
enumerable: true,
|
|
443
|
+
configurable: true,
|
|
444
|
+
});
|
|
445
|
+
config = safeConfig;
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
let response: AxiosResponse;
|
|
449
|
+
|
|
450
|
+
switch (method) {
|
|
451
|
+
case "get":
|
|
452
|
+
response = await axios.get(url, config);
|
|
453
|
+
break;
|
|
454
|
+
case "head":
|
|
455
|
+
response = await axios.head(url, config);
|
|
456
|
+
break;
|
|
457
|
+
case "options":
|
|
458
|
+
response = await axios.options(url, config);
|
|
459
|
+
break;
|
|
460
|
+
case "post":
|
|
461
|
+
response = await axios.post(url, body, config);
|
|
462
|
+
break;
|
|
463
|
+
case "put":
|
|
464
|
+
response = await axios.put(url, body, config);
|
|
465
|
+
break;
|
|
466
|
+
case "patch":
|
|
467
|
+
response = await axios.patch(url, body, config);
|
|
468
|
+
break;
|
|
469
|
+
case "delete":
|
|
470
|
+
response = await axios.delete(url, config);
|
|
471
|
+
break;
|
|
472
|
+
case "request":
|
|
473
|
+
response = await axios.request(
|
|
474
|
+
config as Parameters<typeof axios.request>[0],
|
|
475
|
+
);
|
|
476
|
+
break;
|
|
477
|
+
default:
|
|
478
|
+
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
479
|
+
}
|
|
831
480
|
|
|
832
481
|
/*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
* use axios module" - so without a check here it is a strictly more
|
|
838
|
-
* capable version of the hole that was reported against the API
|
|
839
|
-
* components: arbitrary method, headers and body against the internal
|
|
840
|
-
* network, with the full response marshalled back into the sandbox
|
|
841
|
-
* and on into the workflow log.
|
|
842
|
-
*
|
|
843
|
-
* It has to live on THIS side of the isolate boundary. The
|
|
844
|
-
* sandbox-side axios shim is attacker-editable - user code can simply
|
|
845
|
-
* redefine it - so a check there guards nothing.
|
|
482
|
+
* Convert AxiosHeaders to a plain object before serializing.
|
|
483
|
+
* JSON.stringify calls AxiosHeaders.toJSON(key) with a truthy key,
|
|
484
|
+
* which makes it join array headers (like set-cookie) with commas.
|
|
485
|
+
* This produces invalid Cookie headers when user code forwards them.
|
|
846
486
|
*/
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
487
|
+
return JSON.stringify({
|
|
488
|
+
status: response.status,
|
|
489
|
+
headers: toPlainHeaders(response.headers),
|
|
490
|
+
data: response.data,
|
|
851
491
|
});
|
|
852
|
-
|
|
853
|
-
await SSRFProtection.validateWebhookTargetIsSafe(effectiveUrl);
|
|
854
|
-
|
|
492
|
+
} catch (err: unknown) {
|
|
855
493
|
/*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
* proxy - sends the request to an arbitrary host:port
|
|
860
|
-
* socketPath - connects to a unix socket (/var/run/docker.sock)
|
|
861
|
-
* transport / adapter - replaces the transport wholesale
|
|
862
|
-
* and a redirect would let a validated public host nominate an
|
|
863
|
-
* internal one on the second hop.
|
|
494
|
+
* If this is an axios error with a response (4xx, 5xx, etc.),
|
|
495
|
+
* return the error details as JSON so the sandbox-side axios
|
|
496
|
+
* wrapper can reconstruct error.response for user code.
|
|
864
497
|
*/
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
let response: AxiosResponse;
|
|
875
|
-
|
|
876
|
-
switch (method) {
|
|
877
|
-
case "get":
|
|
878
|
-
response = await axios.get(url, config);
|
|
879
|
-
break;
|
|
880
|
-
case "head":
|
|
881
|
-
response = await axios.head(url, config);
|
|
882
|
-
break;
|
|
883
|
-
case "options":
|
|
884
|
-
response = await axios.options(url, config);
|
|
885
|
-
break;
|
|
886
|
-
case "post":
|
|
887
|
-
response = await axios.post(url, body, config);
|
|
888
|
-
break;
|
|
889
|
-
case "put":
|
|
890
|
-
response = await axios.put(url, body, config);
|
|
891
|
-
break;
|
|
892
|
-
case "patch":
|
|
893
|
-
response = await axios.patch(url, body, config);
|
|
894
|
-
break;
|
|
895
|
-
case "delete":
|
|
896
|
-
response = await axios.delete(url, config);
|
|
897
|
-
break;
|
|
898
|
-
case "request":
|
|
899
|
-
response = await axios.request(
|
|
900
|
-
config as Parameters<typeof axios.request>[0],
|
|
901
|
-
);
|
|
902
|
-
break;
|
|
903
|
-
default:
|
|
904
|
-
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
905
|
-
}
|
|
498
|
+
const axiosErr: {
|
|
499
|
+
isAxiosError?: boolean;
|
|
500
|
+
response?: AxiosResponse<any, any, Record<string, unknown>>;
|
|
501
|
+
message?: string;
|
|
502
|
+
} = err as {
|
|
503
|
+
isAxiosError?: boolean;
|
|
504
|
+
response?: AxiosResponse;
|
|
505
|
+
message?: string;
|
|
506
|
+
};
|
|
906
507
|
|
|
907
|
-
|
|
908
|
-
* Convert AxiosHeaders to a plain object before serializing.
|
|
909
|
-
* JSON.stringify calls AxiosHeaders.toJSON(key) with a truthy key,
|
|
910
|
-
* which makes it join array headers (like set-cookie) with commas.
|
|
911
|
-
* This produces invalid Cookie headers when user code forwards them.
|
|
912
|
-
*/
|
|
508
|
+
if (axiosErr.isAxiosError && axiosErr.response) {
|
|
913
509
|
return JSON.stringify({
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
510
|
+
__isAxiosError: true,
|
|
511
|
+
message: axiosErr.message || "Request failed",
|
|
512
|
+
status: axiosErr.response.status,
|
|
513
|
+
statusText: axiosErr.response.statusText,
|
|
514
|
+
headers: toPlainHeaders(axiosErr.response.headers),
|
|
515
|
+
data: axiosErr.response.data,
|
|
917
516
|
});
|
|
918
|
-
}
|
|
919
|
-
/*
|
|
920
|
-
* If this is an axios error with a response (4xx, 5xx, etc.),
|
|
921
|
-
* return the error details as JSON so the sandbox-side axios
|
|
922
|
-
* wrapper can reconstruct error.response for user code.
|
|
923
|
-
*/
|
|
924
|
-
const axiosErr: {
|
|
925
|
-
isAxiosError?: boolean;
|
|
926
|
-
response?: AxiosResponse<any, any, Record<string, unknown>>;
|
|
927
|
-
message?: string;
|
|
928
|
-
} = err as {
|
|
929
|
-
isAxiosError?: boolean;
|
|
930
|
-
response?: AxiosResponse;
|
|
931
|
-
message?: string;
|
|
932
|
-
};
|
|
517
|
+
}
|
|
933
518
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
message: axiosErr.message || "Request failed",
|
|
938
|
-
status: axiosErr.response.status,
|
|
939
|
-
statusText: axiosErr.response.statusText,
|
|
940
|
-
headers: toPlainHeaders(axiosErr.response.headers),
|
|
941
|
-
data: axiosErr.response.data,
|
|
942
|
-
});
|
|
943
|
-
}
|
|
519
|
+
throw err;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
944
522
|
|
|
945
|
-
|
|
523
|
+
const axiosStartCallback: ivm.Callback<
|
|
524
|
+
(method: string, url: string, arg1?: string, arg2?: string) => string
|
|
525
|
+
> = new ivm.Callback(
|
|
526
|
+
(method: string, url: string, arg1?: string, arg2?: string): string => {
|
|
527
|
+
const operationId: string = String(++nextAxiosOperationId);
|
|
528
|
+
|
|
529
|
+
if (!acceptingHostOperations) {
|
|
530
|
+
return operationId;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (pendingAxiosOperations.size >= 100) {
|
|
534
|
+
pendingAxiosOperations.set(operationId, {
|
|
535
|
+
status: "rejected",
|
|
536
|
+
errorMessage: "Too many pending HTTP requests",
|
|
537
|
+
});
|
|
538
|
+
return operationId;
|
|
946
539
|
}
|
|
540
|
+
|
|
541
|
+
pendingAxiosOperations.set(operationId, { status: "pending" });
|
|
542
|
+
const abortController: AbortController = new AbortController();
|
|
543
|
+
pendingAxiosControllers.add(abortController);
|
|
544
|
+
void executeAxiosRequest(
|
|
545
|
+
abortController.signal,
|
|
546
|
+
method,
|
|
547
|
+
url,
|
|
548
|
+
arg1,
|
|
549
|
+
arg2,
|
|
550
|
+
)
|
|
551
|
+
.then(
|
|
552
|
+
(value: string) => {
|
|
553
|
+
if (pendingAxiosOperations.has(operationId)) {
|
|
554
|
+
pendingAxiosOperations.set(operationId, {
|
|
555
|
+
status: "fulfilled",
|
|
556
|
+
value,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
(error: unknown) => {
|
|
561
|
+
if (pendingAxiosOperations.has(operationId)) {
|
|
562
|
+
pendingAxiosOperations.set(operationId, {
|
|
563
|
+
status: "rejected",
|
|
564
|
+
errorMessage: sanitizeScriptError(error).message,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
)
|
|
569
|
+
.finally(() => {
|
|
570
|
+
pendingAxiosControllers.delete(abortController);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
return operationId;
|
|
947
574
|
},
|
|
575
|
+
{ async: true },
|
|
948
576
|
);
|
|
949
577
|
|
|
950
|
-
|
|
578
|
+
const axiosPollCallback: ivm.Callback<(operationId: string) => string> =
|
|
579
|
+
new ivm.Callback(
|
|
580
|
+
(operationId: string): string => {
|
|
581
|
+
const operation: PendingAxiosOperation | undefined =
|
|
582
|
+
pendingAxiosOperations.get(operationId);
|
|
583
|
+
|
|
584
|
+
if (!operation) {
|
|
585
|
+
return JSON.stringify({
|
|
586
|
+
status: "rejected",
|
|
587
|
+
errorMessage: "HTTP request result is unavailable",
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (operation.status !== "pending") {
|
|
592
|
+
pendingAxiosOperations.delete(operationId);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return JSON.stringify(operation);
|
|
596
|
+
},
|
|
597
|
+
{ async: true },
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
await jail.set("__oneuptimeHostAxiosStartCallback", axiosStartCallback);
|
|
601
|
+
await jail.set("__oneuptimeHostAxiosPollCallback", axiosPollCallback);
|
|
951
602
|
|
|
952
603
|
await context.eval(`
|
|
953
|
-
|
|
604
|
+
(() => {
|
|
605
|
+
const hostAxiosStart = globalThis.__oneuptimeHostAxiosStartCallback;
|
|
606
|
+
const hostAxiosPoll = globalThis.__oneuptimeHostAxiosPollCallback;
|
|
607
|
+
delete globalThis.__oneuptimeHostAxiosStartCallback;
|
|
608
|
+
delete globalThis.__oneuptimeHostAxiosPollCallback;
|
|
609
|
+
const axiosPollWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
610
|
+
|
|
611
|
+
async function hostAxios(method, url, arg1, arg2) {
|
|
612
|
+
const operationId = await hostAxiosStart(method, url, arg1, arg2);
|
|
613
|
+
|
|
614
|
+
while (true) {
|
|
615
|
+
const operation = JSON.parse(await hostAxiosPoll(operationId));
|
|
616
|
+
|
|
617
|
+
if (operation.status === 'pending') {
|
|
618
|
+
Atomics.wait(axiosPollWaitArray, 0, 0, 1);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (operation.status === 'rejected') {
|
|
623
|
+
throw new Error(operation.errorMessage);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return operation.value;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function assertNoFunctions(obj, path) {
|
|
954
631
|
if (!obj || typeof obj !== 'object') return;
|
|
955
632
|
if (Array.isArray(obj)) {
|
|
956
633
|
for (let i = 0; i < obj.length; i++) {
|
|
@@ -962,7 +639,7 @@ export default class VMRunner {
|
|
|
962
639
|
);
|
|
963
640
|
}
|
|
964
641
|
if (obj[i] && typeof obj[i] === 'object') {
|
|
965
|
-
|
|
642
|
+
assertNoFunctions(obj[i], fullPath);
|
|
966
643
|
}
|
|
967
644
|
}
|
|
968
645
|
return;
|
|
@@ -976,12 +653,12 @@ export default class VMRunner {
|
|
|
976
653
|
);
|
|
977
654
|
}
|
|
978
655
|
if (obj[key] && typeof obj[key] === 'object') {
|
|
979
|
-
|
|
656
|
+
assertNoFunctions(obj[key], fullPath);
|
|
980
657
|
}
|
|
981
658
|
}
|
|
982
659
|
}
|
|
983
660
|
|
|
984
|
-
function
|
|
661
|
+
function parseAxiosResult(r) {
|
|
985
662
|
const parsed = JSON.parse(r);
|
|
986
663
|
if (parsed && parsed.__isAxiosError) {
|
|
987
664
|
const err = new Error(parsed.message);
|
|
@@ -998,7 +675,7 @@ export default class VMRunner {
|
|
|
998
675
|
return parsed;
|
|
999
676
|
}
|
|
1000
677
|
|
|
1001
|
-
function
|
|
678
|
+
function makeAxiosInstance(defaults) {
|
|
1002
679
|
function mergeConfig(overrides) {
|
|
1003
680
|
if (!defaults && !overrides) return undefined;
|
|
1004
681
|
if (!defaults) return overrides;
|
|
@@ -1012,9 +689,9 @@ export default class VMRunner {
|
|
|
1012
689
|
|
|
1013
690
|
async function _request(config) {
|
|
1014
691
|
const merged = mergeConfig(config);
|
|
1015
|
-
if (merged)
|
|
1016
|
-
const r = await
|
|
1017
|
-
return
|
|
692
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
693
|
+
const r = await hostAxios('request', '', merged ? JSON.stringify(merged) : undefined);
|
|
694
|
+
return parseAxiosResult(r);
|
|
1018
695
|
}
|
|
1019
696
|
|
|
1020
697
|
// Make instance callable: axios(config) or axios(url, config)
|
|
@@ -1028,141 +705,258 @@ export default class VMRunner {
|
|
|
1028
705
|
instance.request = _request;
|
|
1029
706
|
instance.get = async (url, config) => {
|
|
1030
707
|
const merged = mergeConfig(config);
|
|
1031
|
-
if (merged)
|
|
1032
|
-
const r = await
|
|
1033
|
-
return
|
|
708
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
709
|
+
const r = await hostAxios('get', url, merged ? JSON.stringify(merged) : undefined);
|
|
710
|
+
return parseAxiosResult(r);
|
|
1034
711
|
};
|
|
1035
712
|
instance.head = async (url, config) => {
|
|
1036
713
|
const merged = mergeConfig(config);
|
|
1037
|
-
if (merged)
|
|
1038
|
-
const r = await
|
|
1039
|
-
return
|
|
714
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
715
|
+
const r = await hostAxios('head', url, merged ? JSON.stringify(merged) : undefined);
|
|
716
|
+
return parseAxiosResult(r);
|
|
1040
717
|
};
|
|
1041
718
|
instance.options = async (url, config) => {
|
|
1042
719
|
const merged = mergeConfig(config);
|
|
1043
|
-
if (merged)
|
|
1044
|
-
const r = await
|
|
1045
|
-
return
|
|
720
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
721
|
+
const r = await hostAxios('options', url, merged ? JSON.stringify(merged) : undefined);
|
|
722
|
+
return parseAxiosResult(r);
|
|
1046
723
|
};
|
|
1047
724
|
instance.post = async (url, data, config) => {
|
|
1048
725
|
const merged = mergeConfig(config);
|
|
1049
|
-
if (data)
|
|
1050
|
-
if (merged)
|
|
1051
|
-
const r = await
|
|
1052
|
-
return
|
|
726
|
+
if (data) assertNoFunctions(data, 'data');
|
|
727
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
728
|
+
const r = await hostAxios('post', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined);
|
|
729
|
+
return parseAxiosResult(r);
|
|
1053
730
|
};
|
|
1054
731
|
instance.put = async (url, data, config) => {
|
|
1055
732
|
const merged = mergeConfig(config);
|
|
1056
|
-
if (data)
|
|
1057
|
-
if (merged)
|
|
1058
|
-
const r = await
|
|
1059
|
-
return
|
|
733
|
+
if (data) assertNoFunctions(data, 'data');
|
|
734
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
735
|
+
const r = await hostAxios('put', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined);
|
|
736
|
+
return parseAxiosResult(r);
|
|
1060
737
|
};
|
|
1061
738
|
instance.patch = async (url, data, config) => {
|
|
1062
739
|
const merged = mergeConfig(config);
|
|
1063
|
-
if (data)
|
|
1064
|
-
if (merged)
|
|
1065
|
-
const r = await
|
|
1066
|
-
return
|
|
740
|
+
if (data) assertNoFunctions(data, 'data');
|
|
741
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
742
|
+
const r = await hostAxios('patch', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined);
|
|
743
|
+
return parseAxiosResult(r);
|
|
1067
744
|
};
|
|
1068
745
|
instance.delete = async (url, config) => {
|
|
1069
746
|
const merged = mergeConfig(config);
|
|
1070
|
-
if (merged)
|
|
1071
|
-
const r = await
|
|
1072
|
-
return
|
|
747
|
+
if (merged) assertNoFunctions(merged, 'config');
|
|
748
|
+
const r = await hostAxios('delete', url, merged ? JSON.stringify(merged) : undefined);
|
|
749
|
+
return parseAxiosResult(r);
|
|
1073
750
|
};
|
|
1074
751
|
instance.create = (instanceDefaults) => {
|
|
1075
|
-
if (instanceDefaults)
|
|
752
|
+
if (instanceDefaults) assertNoFunctions(instanceDefaults, 'defaults');
|
|
1076
753
|
const combinedDefaults = mergeConfig(instanceDefaults);
|
|
1077
|
-
return
|
|
754
|
+
return makeAxiosInstance(combinedDefaults);
|
|
1078
755
|
};
|
|
1079
756
|
|
|
1080
757
|
return instance;
|
|
1081
758
|
}
|
|
1082
759
|
|
|
1083
|
-
|
|
760
|
+
Object.defineProperty(globalThis, 'axios', {
|
|
761
|
+
value: makeAxiosInstance(null),
|
|
762
|
+
writable: false,
|
|
763
|
+
configurable: false,
|
|
764
|
+
});
|
|
765
|
+
})();
|
|
1084
766
|
`);
|
|
1085
767
|
|
|
1086
|
-
// crypto (createHash, createHmac, randomBytes, randomUUID, randomInt)
|
|
1087
|
-
const
|
|
768
|
+
// crypto (createHash, createHmac, randomBytes, randomUUID, randomInt)
|
|
769
|
+
const cryptoCallback: ivm.Callback<
|
|
1088
770
|
(op: string, ...args: string[]) => string
|
|
1089
|
-
> = new ivm.
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
771
|
+
> = new ivm.Callback(
|
|
772
|
+
(op: string, ...args: string[]): string => {
|
|
773
|
+
switch (op) {
|
|
774
|
+
case "createHash": {
|
|
775
|
+
const [algorithm, inputData, encoding] = args;
|
|
776
|
+
return crypto
|
|
777
|
+
.createHash(algorithm!)
|
|
778
|
+
.update(inputData!)
|
|
779
|
+
.digest((encoding as crypto.BinaryToTextEncoding) || "hex");
|
|
780
|
+
}
|
|
781
|
+
case "createHmac": {
|
|
782
|
+
const [algorithm, key, inputData, encoding] = args;
|
|
783
|
+
return crypto
|
|
784
|
+
.createHmac(algorithm!, key!)
|
|
785
|
+
.update(inputData!)
|
|
786
|
+
.digest((encoding as crypto.BinaryToTextEncoding) || "hex");
|
|
787
|
+
}
|
|
788
|
+
case "randomBytes": {
|
|
789
|
+
const [size] = args;
|
|
790
|
+
return crypto.randomBytes(parseInt(size!)).toString("hex");
|
|
791
|
+
}
|
|
792
|
+
case "randomUUID": {
|
|
793
|
+
return crypto.randomUUID();
|
|
794
|
+
}
|
|
795
|
+
case "randomInt": {
|
|
796
|
+
const [min, max] = args;
|
|
797
|
+
return String(crypto.randomInt(parseInt(min!), parseInt(max!)));
|
|
798
|
+
}
|
|
799
|
+
default:
|
|
800
|
+
throw new Error(`Unsupported crypto operation: ${op}`);
|
|
1115
801
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
});
|
|
802
|
+
},
|
|
803
|
+
{ sync: true },
|
|
804
|
+
);
|
|
1120
805
|
|
|
1121
|
-
await jail.set("
|
|
806
|
+
await jail.set("__oneuptimeHostCryptoCallback", cryptoCallback);
|
|
1122
807
|
|
|
1123
808
|
await context.eval(`
|
|
1124
|
-
|
|
809
|
+
(() => {
|
|
810
|
+
const hostCrypto = globalThis.__oneuptimeHostCryptoCallback;
|
|
811
|
+
delete globalThis.__oneuptimeHostCryptoCallback;
|
|
812
|
+
|
|
813
|
+
const sandboxCrypto = {
|
|
1125
814
|
createHash: (algorithm) => ({
|
|
1126
815
|
_alg: algorithm, _data: '',
|
|
1127
816
|
update(d) { this._data = d; return this; },
|
|
1128
|
-
digest(enc) { return
|
|
817
|
+
digest(enc) { return hostCrypto('createHash', this._alg, this._data, enc || 'hex'); }
|
|
1129
818
|
}),
|
|
1130
819
|
createHmac: (algorithm, key) => ({
|
|
1131
820
|
_alg: algorithm, _key: key, _data: '',
|
|
1132
821
|
update(d) { this._data = d; return this; },
|
|
1133
|
-
digest(enc) { return
|
|
822
|
+
digest(enc) { return hostCrypto('createHmac', this._alg, this._key, this._data, enc || 'hex'); }
|
|
1134
823
|
}),
|
|
1135
824
|
randomBytes: (size) => ({
|
|
1136
|
-
toString(enc) { return
|
|
825
|
+
toString(enc) { return hostCrypto('randomBytes', String(size)); }
|
|
1137
826
|
}),
|
|
1138
827
|
randomUUID: () => {
|
|
1139
|
-
return
|
|
828
|
+
return hostCrypto('randomUUID');
|
|
1140
829
|
},
|
|
1141
830
|
randomInt: (minOrMax, max) => {
|
|
1142
831
|
if (max === undefined) { max = minOrMax; minOrMax = 0; }
|
|
1143
|
-
return Number(
|
|
832
|
+
return Number(hostCrypto('randomInt', String(minOrMax), String(max)));
|
|
1144
833
|
},
|
|
1145
834
|
};
|
|
1146
|
-
`);
|
|
1147
835
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
global.setTimeout(resolve, Math.min(ms, timeout));
|
|
1153
|
-
});
|
|
836
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
837
|
+
value: sandboxCrypto,
|
|
838
|
+
writable: false,
|
|
839
|
+
configurable: false,
|
|
1154
840
|
});
|
|
841
|
+
})();
|
|
842
|
+
`);
|
|
843
|
+
|
|
844
|
+
// setTimeout / sleep - bridged through copied start/poll callbacks
|
|
845
|
+
const sleepStartCallback: ivm.Callback<(ms: number) => string> =
|
|
846
|
+
new ivm.Callback(
|
|
847
|
+
(ms: number): string => {
|
|
848
|
+
const operationId: string = String(++nextSleepOperationId);
|
|
849
|
+
|
|
850
|
+
if (!acceptingHostOperations) {
|
|
851
|
+
return operationId;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const numericDelay: number = Number(ms);
|
|
855
|
+
const boundedDelay: number = Number.isFinite(numericDelay)
|
|
856
|
+
? Math.max(0, Math.min(numericDelay, timeout))
|
|
857
|
+
: 0;
|
|
858
|
+
const timeoutHandle: ReturnType<typeof global.setTimeout> =
|
|
859
|
+
global.setTimeout(() => {
|
|
860
|
+
const operation: PendingSleepOperation | undefined =
|
|
861
|
+
pendingSleepOperations.get(operationId);
|
|
862
|
+
|
|
863
|
+
if (operation) {
|
|
864
|
+
operation.settled = true;
|
|
865
|
+
}
|
|
866
|
+
pendingHostTimeouts.delete(timeoutHandle);
|
|
867
|
+
}, boundedDelay);
|
|
868
|
+
|
|
869
|
+
pendingHostTimeouts.add(timeoutHandle);
|
|
870
|
+
pendingSleepOperations.set(operationId, {
|
|
871
|
+
settled: false,
|
|
872
|
+
});
|
|
1155
873
|
|
|
1156
|
-
|
|
874
|
+
return operationId;
|
|
875
|
+
},
|
|
876
|
+
{ async: true },
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const sleepPollCallback: ivm.Callback<(operationId: string) => boolean> =
|
|
880
|
+
new ivm.Callback(
|
|
881
|
+
(operationId: string): boolean => {
|
|
882
|
+
const operation: PendingSleepOperation | undefined =
|
|
883
|
+
pendingSleepOperations.get(operationId);
|
|
884
|
+
|
|
885
|
+
if (!operation || operation.settled) {
|
|
886
|
+
pendingSleepOperations.delete(operationId);
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return false;
|
|
891
|
+
},
|
|
892
|
+
{ async: true },
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
await jail.set("__oneuptimeHostSleepStartCallback", sleepStartCallback);
|
|
896
|
+
await jail.set("__oneuptimeHostSleepPollCallback", sleepPollCallback);
|
|
1157
897
|
|
|
1158
898
|
await context.eval(`
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
899
|
+
(() => {
|
|
900
|
+
const hostSleepStart = globalThis.__oneuptimeHostSleepStartCallback;
|
|
901
|
+
const hostSleepPoll = globalThis.__oneuptimeHostSleepPollCallback;
|
|
902
|
+
delete globalThis.__oneuptimeHostSleepStartCallback;
|
|
903
|
+
delete globalThis.__oneuptimeHostSleepPollCallback;
|
|
904
|
+
const activeTimers = new WeakSet();
|
|
905
|
+
const pollWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
906
|
+
|
|
907
|
+
async function hostSleep(ms) {
|
|
908
|
+
const operationId = await hostSleepStart(ms || 0);
|
|
909
|
+
|
|
910
|
+
while (!(await hostSleepPoll(operationId))) {
|
|
911
|
+
// Pace polling on the isolate worker without blocking Node's
|
|
912
|
+
// event loop.
|
|
913
|
+
Atomics.wait(pollWaitArray, 0, 0, 1);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function sandboxSetTimeout(fn, ms, ...args) {
|
|
918
|
+
if (typeof fn !== 'function') {
|
|
919
|
+
throw new TypeError('setTimeout callback must be a function');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const handle = {};
|
|
923
|
+
activeTimers.add(handle);
|
|
924
|
+
hostSleep(ms || 0).then(() => {
|
|
925
|
+
if (activeTimers.delete(handle)) {
|
|
926
|
+
fn(...args);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
return handle;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function sandboxClearTimeout(handle) {
|
|
933
|
+
if (handle && typeof handle === 'object') {
|
|
934
|
+
activeTimers.delete(handle);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function sandboxSleep(ms) {
|
|
939
|
+
await hostSleep(ms || 0);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
Object.defineProperties(globalThis, {
|
|
943
|
+
setTimeout: {
|
|
944
|
+
value: sandboxSetTimeout,
|
|
945
|
+
writable: false,
|
|
946
|
+
configurable: false,
|
|
947
|
+
},
|
|
948
|
+
clearTimeout: {
|
|
949
|
+
value: sandboxClearTimeout,
|
|
950
|
+
writable: false,
|
|
951
|
+
configurable: false,
|
|
952
|
+
},
|
|
953
|
+
sleep: {
|
|
954
|
+
value: sandboxSleep,
|
|
955
|
+
writable: false,
|
|
956
|
+
configurable: false,
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
})();
|
|
1166
960
|
`);
|
|
1167
961
|
|
|
1168
962
|
/*
|
|
@@ -1179,24 +973,35 @@ export default class VMRunner {
|
|
|
1179
973
|
catch(_) { return undefined; }
|
|
1180
974
|
})()`;
|
|
1181
975
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
promise: true,
|
|
1185
|
-
timeout: timeout,
|
|
1186
|
-
});
|
|
976
|
+
let result: unknown;
|
|
977
|
+
let scriptError: Error | undefined;
|
|
1187
978
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
|
|
979
|
+
try {
|
|
980
|
+
// Run with overall timeout covering both CPU and I/O wait.
|
|
981
|
+
const resultPromise: Promise<unknown> = context.eval(wrappedCode, {
|
|
982
|
+
promise: true,
|
|
983
|
+
timeout: timeout,
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const overallTimeout: Promise<never> = new Promise(
|
|
987
|
+
(
|
|
988
|
+
_resolve: (value: never) => void,
|
|
989
|
+
reject: (reason: Error) => void,
|
|
990
|
+
) => {
|
|
991
|
+
const timeoutHandle: ReturnType<typeof global.setTimeout> =
|
|
992
|
+
global.setTimeout(() => {
|
|
993
|
+
pendingHostTimeouts.delete(timeoutHandle);
|
|
994
|
+
reject(new Error("Script execution timed out"));
|
|
995
|
+
}, timeout + 5000); // 5s grace period beyond isolate timeout
|
|
1195
996
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
997
|
+
pendingHostTimeouts.add(timeoutHandle);
|
|
998
|
+
},
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
result = await Promise.race([resultPromise, overallTimeout]);
|
|
1002
|
+
} catch (error: unknown) {
|
|
1003
|
+
scriptError = sanitizeScriptError(error);
|
|
1004
|
+
}
|
|
1200
1005
|
|
|
1201
1006
|
// Parse the JSON string returned from inside the isolate
|
|
1202
1007
|
let returnValue: unknown;
|
|
@@ -1215,8 +1020,22 @@ export default class VMRunner {
|
|
|
1215
1020
|
returnValue,
|
|
1216
1021
|
logMessages,
|
|
1217
1022
|
capturedMetrics,
|
|
1023
|
+
scriptError,
|
|
1218
1024
|
};
|
|
1219
1025
|
} finally {
|
|
1026
|
+
acceptingHostOperations = false;
|
|
1027
|
+
|
|
1028
|
+
for (const timeoutHandle of pendingHostTimeouts) {
|
|
1029
|
+
global.clearTimeout(timeoutHandle);
|
|
1030
|
+
}
|
|
1031
|
+
for (const abortController of pendingAxiosControllers) {
|
|
1032
|
+
abortController.abort();
|
|
1033
|
+
}
|
|
1034
|
+
pendingHostTimeouts.clear();
|
|
1035
|
+
pendingAxiosControllers.clear();
|
|
1036
|
+
pendingSleepOperations.clear();
|
|
1037
|
+
pendingAxiosOperations.clear();
|
|
1038
|
+
|
|
1220
1039
|
if (!isolate.isDisposed) {
|
|
1221
1040
|
isolate.dispose();
|
|
1222
1041
|
}
|