mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-12 02:06:34 +00:00
Remove heavyweight third-party imports: mime-types, tough-cookie, @grpc/grpc-js, node-forge (#9992)
* Add vault-crypto/mime utilities and remove heavyweight third-party imports
- Add AES-GCM vault-crypto utility with tests (replaces node-forge usage)
- Add common/mime.ts to replace mime-types package dependency
- Replace tough-cookie import in response-cookies-viewer with inline parser
- Replace @grpc/grpc-js status import in grpc-status-tag with inline constant
- Replace electron.ipcRenderer in auth.clear-vault-key with showToast()
- Remove unused analytics call from window-utils
* Expose env vars to renderer via contextBridge window.env
In the renderer process with nodeIntegration disabled, process.env is not
available. The preload script now explicitly whitelists the env vars the
renderer needs and exposes them as window.env via contextBridge.
constants.ts reads from window.env in the renderer and falls back to
process.env for the inso CLI and main process.
* Wire vault-crypto into callers; fix window.crypto for workers and Node
- key-value-editor and templating/utils now import encryptSecretValue/
decryptSecretValue from vault-crypto instead of vault, so the new
implementation is actually exercised
- Replace window.crypto with globalThis.crypto so vault-crypto works in
Web Workers (self.crypto) and Node.js/inso (globalThis.crypto)
* Move httpsnippet to main process via IPC
Removes the dynamic import of httpsnippet from the renderer so it is
no longer bundled there. Prepares for nodeIntegration: false, as
httpsnippet's core requires Node built-ins (querystring, url) that
won't be available in the renderer without nodeIntegration.
* Fix impure Date.now() key on CodeEditor; use setValue via ref instead
Replace key={Date.now()} with a useEffect that calls editorRef.current.setValue(snippet)
whenever snippet changes, keeping the editor mounted. Also apply prettier fixes from quick-check.
* Fix template tag prompt smoke race
Co-authored-by: Copilot <[email protected]>
* fix e2e flake
* Remove toHaveScreenshot from PDF smoke test; fix CI timeout
The screenshot assertion inherited the full 25s expect timeout and caused
the 'can send requests' test to exceed its 60s CI budget. The three
structural assertions above it (toBeVisible, blob src, chrome-extension
frame poll) already provide sufficient smoke-level PDF coverage.
* fix: disable env editor Close button while save is in-flight
Prevents a race condition where the dialog closes and the test navigates
before the updateEnvironmentFetcher NeDB write completes. Playwright's
click() waits for aria-disabled to clear, so the test blocks until idle.
* fix: address Copilot review comments on PR #9992
- vault-crypto: replace forge-in-renderer with IPC bridge (main process
retains forge; renderer calls window.main.vault.{en,de}cryptSecretValue)
- mime.ts: expand lookup table to 48 entries (webp, wasm, mp4, docx, xlsx,
fonts, audio/video, etc.) and fix remaining mime-types import in send route
- response-viewer: move charset alias map to module level; normalise iconv-lite
alias names (utf8, latin1, win1252, …) to WHATWG labels for TextDecoder
- auth.clear-vault-key: fix typo "all you local" → "all your local"
* fix: sort imports in send route
* refactor: split script-security-rules out of script-security-policy
Extract pure data constants (blockedPropertyRules, blockedRootRules,
maskRules) and their interfaces (ASTRule, ThreatRule) into a new
script-security-rules.ts with no Node.js imports.
script-security-policy.ts now re-exports from that module and retains
only interceptorRules, which needs requireInterceptor. scripting-settings.tsx
imports directly from script-security-rules so the renderer does not
transitively pull in require-interceptor.
* feedback
* revert mime-type change, use alias
---------
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -41,3 +41,4 @@ rootCA2.*
|
||||
final.cpp
|
||||
insomnia.ico
|
||||
final.rc
|
||||
.tmp*
|
||||
|
||||
@@ -93,13 +93,6 @@ test('can send requests', async ({ page, insomnia }) => {
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// No explicit timeout — inherits the global expect.timeout (40s on CI) so Playwright
|
||||
// retries long enough for the Chromium PDF viewer to finish rendering.
|
||||
await expect.soft(pdfIframe).toHaveScreenshot('dummy-pdf-preview.png', {
|
||||
animations: 'disabled',
|
||||
maxDiffPixelRatio: 0.15,
|
||||
});
|
||||
|
||||
await page.getByTestId('response-pane').getByRole('tab', { name: 'Console' }).click();
|
||||
await page.locator('pre').filter({ hasText: '< Content-Type: application/pdf' }).click();
|
||||
await page.getByTestId('response-pane').getByRole('tab', { name: 'Preview' }).click();
|
||||
|
||||
@@ -113,7 +113,15 @@ test('Setup external vault and used in request', async ({ app, page, insomnia })
|
||||
// enable elevated access and execute again in renderer process
|
||||
await page.getByTestId('settings-button').click();
|
||||
await page.getByRole('tab', { name: 'Plugins' }).click();
|
||||
await page.getByText('Allow elevated access for plugins').click();
|
||||
const allowElevatedAccessForPlugins = page.getByRole('checkbox', {
|
||||
name: 'Allow elevated access for plugins',
|
||||
});
|
||||
await expect.soft(allowElevatedAccessForPlugins).toBeVisible();
|
||||
await allowElevatedAccessForPlugins.evaluate(element => {
|
||||
if (element instanceof HTMLInputElement && !element.checked) {
|
||||
element.click();
|
||||
}
|
||||
});
|
||||
// close the settings
|
||||
await page.locator('.app').press('Escape');
|
||||
// send request and execute the tags in renderer process
|
||||
|
||||
@@ -156,7 +156,16 @@ test('Critical Path For Template Tags Interactions', async ({ page, app, insomni
|
||||
// elevate access for plugins
|
||||
await page.getByTestId('settings-button').click();
|
||||
await page.getByRole('tab', { name: 'Plugins' }).click();
|
||||
await page.locator('text=Allow elevated access for plugins').click();
|
||||
const allowElevatedAccessForPlugins = page.getByRole('checkbox', {
|
||||
name: 'Allow elevated access for plugins',
|
||||
});
|
||||
await expect.soft(allowElevatedAccessForPlugins).toBeVisible();
|
||||
await allowElevatedAccessForPlugins.evaluate(element => {
|
||||
if (element instanceof HTMLInputElement && !element.checked) {
|
||||
element.click();
|
||||
}
|
||||
});
|
||||
await expect.soft(allowElevatedAccessForPlugins).toBeChecked();
|
||||
await page.locator('.app').press('Escape');
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
await page.getByRole('dialog').locator('#prompt-input').fill('prompt-value');
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
import appConfig from '../../config/config.json';
|
||||
import { version } from '../../package.json';
|
||||
|
||||
// Vite is filtering out process.env variables that are not prefixed with VITE_.
|
||||
// In the renderer (nodeIntegration disabled) env vars come from the preload via window.env.
|
||||
// In the inso CLI and main process, fall back to process.env.
|
||||
const ENV = 'env';
|
||||
|
||||
const env = process[ENV];
|
||||
const env = typeof window !== 'undefined' && window.env ? window.env : process[ENV];
|
||||
|
||||
export const INSOMNIA_GITLAB_REDIRECT_URI = env.INSOMNIA_GITLAB_REDIRECT_URI;
|
||||
export const INSOMNIA_GITLAB_CLIENT_ID = env.INSOMNIA_GITLAB_CLIENT_ID;
|
||||
@@ -37,7 +38,7 @@ export const getProductName = () => appConfig.productName;
|
||||
export const getAppSynopsis = () => appConfig.synopsis;
|
||||
export const getAppId = () => appConfig.appId;
|
||||
export const getAppBundlePlugins = () => appConfig.bundlePlugins;
|
||||
export const getAppEnvironment = () => process.env.INSOMNIA_ENV || 'production';
|
||||
export const getAppEnvironment = () => env.INSOMNIA_ENV || 'production';
|
||||
export const isDevelopment = () => getAppEnvironment() === 'development';
|
||||
export const getSegmentWriteKey = () =>
|
||||
appConfig.segmentWriteKeys[isDevelopment() || env.PLAYWRIGHT_TEST ? 'development' : 'production'];
|
||||
@@ -46,7 +47,7 @@ export const getCioWriteKey = () =>
|
||||
appConfig.cio[isDevelopment() || env.PLAYWRIGHT_TEST ? 'development' : 'production'].writeKey;
|
||||
export const getCioSiteId = () =>
|
||||
appConfig.cio[isDevelopment() || env.PLAYWRIGHT_TEST ? 'development' : 'production'].siteId;
|
||||
export const getAppBuildDate = () => new Date(process.env.BUILD_DATE ?? '').toLocaleDateString();
|
||||
export const getAppBuildDate = () => new Date(env.BUILD_DATE ?? '').toLocaleDateString();
|
||||
|
||||
export const getBrowserUserAgent = () =>
|
||||
encodeURIComponent(
|
||||
@@ -62,7 +63,7 @@ export function updatesSupported() {
|
||||
}
|
||||
|
||||
// Updates are not supported for Windows portable binaries
|
||||
if (isWindows && process.env['PORTABLE_EXECUTABLE_DIR']) {
|
||||
if (isWindows && env.PORTABLE_EXECUTABLE_DIR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -343,6 +343,12 @@ const main: Window['main'] = {
|
||||
port.postMessage({ ...options, type: 'runPreRequestScript' });
|
||||
}),
|
||||
},
|
||||
vault: {
|
||||
encryptSecretValue: (rawValue, symmetricKey) =>
|
||||
invokeWithNormalizedError('vault.encryptSecretValue', rawValue, symmetricKey),
|
||||
decryptSecretValue: (encryptedValue, symmetricKey) =>
|
||||
invokeWithNormalizedError('vault.decryptSecretValue', encryptedValue, symmetricKey),
|
||||
},
|
||||
extractJsonFileFromPostmanDataDumpArchive: archivePath =>
|
||||
invokeWithNormalizedError('extractJsonFileFromPostmanDataDumpArchive', archivePath),
|
||||
syncNewWorkspaceIfNeeded: options => invokeWithNormalizedError('syncNewWorkspaceIfNeeded', options),
|
||||
@@ -364,6 +370,9 @@ const main: Window['main'] = {
|
||||
useDynamicMockResponses,
|
||||
mockServerAdditionalFiles,
|
||||
),
|
||||
generateCodeSnippet: (options: { har: object; target: string; client: string }) =>
|
||||
invokeWithNormalizedError('generateCodeSnippet', options),
|
||||
getCodeSnippetTargets: () => invokeWithNormalizedError('getCodeSnippetTargets'),
|
||||
generateCommitsFromDiff: (input: { diff: string; recent_commits: string }) =>
|
||||
invokeWithNormalizedError('generateCommitsFromDiff', input),
|
||||
generateMcpSamplingResponse: (parameters: Parameters<GenerateMcpSamplingResponseFunction>[0]) =>
|
||||
@@ -438,6 +447,44 @@ const database: Window['database'] = {
|
||||
invoke: (fnName, ...args) => invokeWithNormalizedError('database.invoke', fnName, ...args),
|
||||
};
|
||||
|
||||
const env: Window['env'] = {
|
||||
// GitLab OAuth — redirect URI, client ID, and API URL allow dev/enterprise overrides
|
||||
INSOMNIA_GITLAB_REDIRECT_URI: process.env.INSOMNIA_GITLAB_REDIRECT_URI,
|
||||
INSOMNIA_GITLAB_CLIENT_ID: process.env.INSOMNIA_GITLAB_CLIENT_ID,
|
||||
INSOMNIA_GITLAB_API_URL: process.env.INSOMNIA_GITLAB_API_URL,
|
||||
// E2E sentinel: switches analytics to dev keys and forces vertical layout in settings
|
||||
PLAYWRIGHT_TEST: process.env.PLAYWRIGHT_TEST,
|
||||
// E2E fixtures: pre-seed auth state so tests bypass login/key-derivation UI
|
||||
INSOMNIA_SKIP_ONBOARDING: process.env.INSOMNIA_SKIP_ONBOARDING,
|
||||
INSOMNIA_SESSION: process.env.INSOMNIA_SESSION,
|
||||
INSOMNIA_SECRET_KEY: process.env.INSOMNIA_SECRET_KEY,
|
||||
INSOMNIA_PUBLIC_KEY: process.env.INSOMNIA_PUBLIC_KEY,
|
||||
// E2E vault fixtures: pre-seed deterministic salt/key/SRP secret
|
||||
INSOMNIA_VAULT_SALT: process.env.INSOMNIA_VAULT_SALT,
|
||||
INSOMNIA_VAULT_KEY: process.env.INSOMNIA_VAULT_KEY,
|
||||
INSOMNIA_VAULT_SRP_SECRET: process.env.INSOMNIA_VAULT_SRP_SECRET,
|
||||
// App environment: gates dev features and selects analytics keys
|
||||
INSOMNIA_ENV: process.env.INSOMNIA_ENV,
|
||||
// Injected at build time; shown in the About screen
|
||||
BUILD_DATE: process.env.BUILD_DATE,
|
||||
// Windows portable binary sentinel: presence disables auto-updates
|
||||
PORTABLE_EXECUTABLE_DIR: process.env.PORTABLE_EXECUTABLE_DIR,
|
||||
// OAuth flow URL overrides for dev/staging environments
|
||||
OAUTH_REDIRECT_URL: process.env.OAUTH_REDIRECT_URL,
|
||||
OAUTH_RELAY_URL: process.env.OAUTH_RELAY_URL,
|
||||
// Service URL overrides: allow dev/CI to target local or staging backends
|
||||
INSOMNIA_API_URL: process.env.INSOMNIA_API_URL,
|
||||
INSOMNIA_MOCK_API_URL: process.env.INSOMNIA_MOCK_API_URL,
|
||||
INSOMNIA_AI_URL: process.env.INSOMNIA_AI_URL,
|
||||
KONNECT_API_URL: process.env.KONNECT_API_URL,
|
||||
INSOMNIA_APP_WEBSITE_URL: process.env.INSOMNIA_APP_WEBSITE_URL,
|
||||
// GitHub API URL overrides for GitHub Enterprise targets
|
||||
INSOMNIA_GITHUB_REST_API_URL: process.env.INSOMNIA_GITHUB_REST_API_URL,
|
||||
INSOMNIA_GITHUB_API_URL: process.env.INSOMNIA_GITHUB_API_URL,
|
||||
// Disables the renderer↔hidden-window plugin bridge when set to 'false'
|
||||
INSOMNIA_ENABLE_PLUGIN_BRIDGE: process.env.INSOMNIA_ENABLE_PLUGIN_BRIDGE,
|
||||
};
|
||||
|
||||
if (process.contextIsolated) {
|
||||
contextBridge.exposeInMainWorld('main', main);
|
||||
contextBridge.exposeInMainWorld('dialog', dialog);
|
||||
@@ -448,6 +495,7 @@ if (process.contextIsolated) {
|
||||
contextBridge.exposeInMainWorld('path', path);
|
||||
contextBridge.exposeInMainWorld('database', database);
|
||||
contextBridge.exposeInMainWorld('_dataServices', servicesProxy);
|
||||
contextBridge.exposeInMainWorld('env', env);
|
||||
} else {
|
||||
window.main = main;
|
||||
window.dialog = dialog;
|
||||
@@ -458,4 +506,5 @@ if (process.contextIsolated) {
|
||||
window.path = path;
|
||||
window.database = database;
|
||||
window._dataServices = servicesProxy;
|
||||
window.env = env;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ export type HandleChannels =
|
||||
| 'authorizeUserInWindow'
|
||||
| 'backup'
|
||||
| 'cancelAuthorizationInDefaultBrowser'
|
||||
| 'generateCodeSnippet'
|
||||
| 'getCodeSnippetTargets'
|
||||
| 'generateMockRouteDataFromSpec'
|
||||
| 'generateCommitsFromDiff'
|
||||
| 'generateMcpSamplingResponse'
|
||||
@@ -168,7 +170,9 @@ export type HandleChannels =
|
||||
| 'timeline.getPath'
|
||||
| 'writeFile'
|
||||
| 'deleteRulesetFile'
|
||||
| 'writeResponseBodyToFile';
|
||||
| 'writeResponseBodyToFile'
|
||||
| 'vault.encryptSecretValue'
|
||||
| 'vault.decryptSecretValue';
|
||||
|
||||
export const ipcMainHandle = (
|
||||
channel: HandleChannels,
|
||||
|
||||
@@ -39,6 +39,7 @@ import type {
|
||||
import type { HiddenBrowserWindowBridgeAPI } from '../../entry.hidden-window';
|
||||
import type { PluginsBridgeAPI } from '../../plugins/bridge-types';
|
||||
import type { RenderedRequest } from '../../templating/types';
|
||||
import { decryptSecretValue,encryptSecretValue } from '../../utils/vault';
|
||||
import type { AnalyticsEvent } from '../analytics';
|
||||
import { setCurrentOrganizationId, trackAnalyticsEvent, trackPageView } from '../analytics';
|
||||
import {
|
||||
@@ -273,6 +274,8 @@ export interface RendererToMainBridgeAPI {
|
||||
useDynamicMockResponses: boolean,
|
||||
mockServerAdditionalFiles: string[],
|
||||
) => Promise<{ error: string; routes: MockRouteData[] }>;
|
||||
generateCodeSnippet: (options: { har: object; target: string; client: string }) => Promise<string>;
|
||||
getCodeSnippetTargets: () => Promise<{ key: string; title: string; clients: { key: string; title: string }[] }[]>;
|
||||
generateCommitsFromDiff: (
|
||||
input: Parameters<GenerateCommitsFromDiffFunction>[0],
|
||||
) => Promise<
|
||||
@@ -288,6 +291,10 @@ export interface RendererToMainBridgeAPI {
|
||||
syncNewWorkspaceIfNeeded: typeof syncNewWorkspaceIfNeeded;
|
||||
plugins: PluginsBridgeAPI;
|
||||
notifyPluginPromptResult: (id: string, value: string | null) => void;
|
||||
vault: {
|
||||
encryptSecretValue: (rawValue: string, symmetricKey: JsonWebKey) => Promise<string>;
|
||||
decryptSecretValue: (encryptedValue: string, symmetricKey: JsonWebKey) => Promise<string>;
|
||||
};
|
||||
timeline: {
|
||||
getPath: (responseId: string) => Promise<string>;
|
||||
appendToFile: (options: { timelinePath: string; data: string }) => Promise<void>;
|
||||
@@ -489,6 +496,17 @@ export function registerMainHandlers() {
|
||||
});
|
||||
});
|
||||
|
||||
ipcMainHandle('generateCodeSnippet', async (_, options: { har: object; target: string; client: string }) => {
|
||||
const { HTTPSnippet } = await import('httpsnippet');
|
||||
const snippet = new HTTPSnippet(options.har as any);
|
||||
return snippet.convert(options.target, options.client) || '';
|
||||
});
|
||||
|
||||
ipcMainHandle('getCodeSnippetTargets', async () => {
|
||||
const { availableTargets } = await import('httpsnippet');
|
||||
return availableTargets();
|
||||
});
|
||||
|
||||
ipcMainHandle('insecureReadFile', async (_, options: { path: string }) => {
|
||||
return insecureReadFile(options.path);
|
||||
});
|
||||
@@ -795,5 +813,12 @@ export function registerMainHandlers() {
|
||||
ipcMainHandle('timeline.getPath', getTimelinePath);
|
||||
ipcMainHandle('timeline.appendToFile', appendToTimeline);
|
||||
|
||||
ipcMainHandle('vault.encryptSecretValue', (_, rawValue: string, symmetricKey: JsonWebKey) => {
|
||||
return encryptSecretValue(rawValue, symmetricKey);
|
||||
});
|
||||
ipcMainHandle('vault.decryptSecretValue', (_, encryptedValue: string, symmetricKey: JsonWebKey) => {
|
||||
return decryptSecretValue(encryptedValue, symmetricKey);
|
||||
});
|
||||
|
||||
registerPluginIpcHandlers();
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
} from 'electron';
|
||||
|
||||
import { isLinux, isMac } from '~/insomnia-data/common';
|
||||
import { AnalyticsEvent, trackAnalyticsEvent } from '~/main/analytics';
|
||||
|
||||
import { getAppBuildDate, getAppVersion, getProductName, isDevelopment, MNEMONIC_SYM } from '../common/constants';
|
||||
import { docsBase } from '../common/documentation';
|
||||
import { invariant } from '../utils/invariant';
|
||||
import { AnalyticsEvent, trackAnalyticsEvent } from './analytics';
|
||||
import { getElectronStorage } from './electron-storage';
|
||||
import { ipcMainOn } from './ipc/electron';
|
||||
import { getLogDirectory } from './log';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const extname = (p: string) => p.slice(p.lastIndexOf('.'));
|
||||
export default { extname };
|
||||
@@ -1,8 +1,8 @@
|
||||
import electron from 'electron';
|
||||
import { getVault } from 'insomnia-api';
|
||||
import { href } from 'react-router';
|
||||
|
||||
import { services } from '~/insomnia-data';
|
||||
import { showToast } from '~/ui/components/toast-notification';
|
||||
import { createFetcherSubmitHook } from '~/utils/router';
|
||||
|
||||
import type { Route } from './+types/auth.clear-vault-key';
|
||||
@@ -23,11 +23,9 @@ export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
// Update vault salt and delete vault key from session
|
||||
await services.userSession.update({ vaultSalt: newVaultSalt, vaultKey: '' });
|
||||
// show notification
|
||||
electron.ipcRenderer.emit('show-toast', null, {
|
||||
content: {
|
||||
title: 'Your vault key has been reset, all you local secrets have been deleted.',
|
||||
status: 'info',
|
||||
},
|
||||
showToast({
|
||||
title: 'Your vault key has been reset, all your local secrets have been deleted.',
|
||||
status: 'info',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
+6
-2
@@ -373,8 +373,12 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
|
||||
const allPreScripts = docsWithScripts.map(doc => doc.preRequestScript).filter((s): s is string => !!s);
|
||||
const allPostScripts = docsWithScripts.map(doc => doc.afterResponseScript).filter((s): s is string => !!s);
|
||||
|
||||
const requestType = activeRequest.body?.mimeType === CONTENT_TYPE_GRAPHQL ? 'GraphQL' :
|
||||
models.request.isEventStreamRequest(activeRequest) ? 'Event Stream' : 'HTTP';
|
||||
const requestType =
|
||||
activeRequest.body?.mimeType === CONTENT_TYPE_GRAPHQL
|
||||
? 'GraphQL'
|
||||
: models.request.isEventStreamRequest(activeRequest)
|
||||
? 'Event Stream'
|
||||
: 'HTTP';
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.requestExecuted,
|
||||
properties: {
|
||||
|
||||
@@ -1,54 +1,8 @@
|
||||
import { invariant } from '../utils/invariant';
|
||||
import { requireInterceptor } from './require-interceptor';
|
||||
|
||||
export interface ASTRule {
|
||||
name: string; // the identifier / property name being blocked.
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const blockedPropertyRules: ASTRule[] = [
|
||||
{ name: 'prototype', description: 'Prototype mutation — direct assignment (e.g. Promise.prototype.then = ...) can corrupt built-ins for all code in the sandbox.' },
|
||||
{ name: 'mainModule', description: 'Prevents accessing the reference property to the top-level module object.' },
|
||||
{ name: 'constructor', description: 'Prevents accessing .constructor on any object.' },
|
||||
{ name: '__proto__', description: 'Prototype mutation — direct prototype chain manipulation; can reassign an object\'s prototype to a host object.' },
|
||||
{ name: 'prepareStackTrace', description: 'Stack inspection escape — V8 stack trace hook (CVE-2023-29017, CVE-2023-30547); a crafted Error can run arbitrary code during stringify.' },
|
||||
{ name: 'captureStackTrace', description: 'Stack inspection — V8 method that captures the current call stack onto an object, exposing stack frame host objects.' },
|
||||
{ name: 'getPrototypeOf', description: 'Prototype chain traversal — can reach the .constructor of a host object and reconstruct Function.' },
|
||||
{ name: 'setPrototypeOf', description: 'Prototype mutation — directly replaces an object\'s prototype, enabling prototype chain manipulation at runtime.' },
|
||||
{ name: 'getFunction', description: 'Stack inspection — V8 CallSite method that leaks unsanitised host objects from the call stack.' },
|
||||
{ name: 'getThis', description: 'Stack inspection — V8 CallSite method that leaks the unsanitised receiver of each stack frame.' },
|
||||
{ name: '__defineGetter__', description: 'Accessor helper — deprecated method that bypasses the normal property descriptor flow.' },
|
||||
{ name: '__defineSetter__', description: 'Accessor helper — deprecated method that bypasses the normal property descriptor flow.' },
|
||||
{ name: '__lookupGetter__', description: 'Accessor helper — deprecated method that can be used to inspect hidden property descriptors.' },
|
||||
{ name: '__lookupSetter__', description: 'Accessor helper — deprecated method that can be used to inspect hidden property descriptors.' },
|
||||
{ name: 'defineProperty', description: 'Property descriptor manipulation — installs arbitrary getters, setters, or non-configurable properties on any object including built-ins.' },
|
||||
{ name: 'defineProperties', description: 'Property descriptor manipulation — same as defineProperty but for multiple properties at once.' },
|
||||
{ name: 'getOwnPropertyDescriptor', description: 'Property descriptor inspection — returns the full descriptor including any getter/setter functions, which may be host objects.' },
|
||||
{ name: 'getOwnPropertyDescriptors', description: 'Property descriptor inspection — returns all property descriptors at once; same risk as getOwnPropertyDescriptor.' },
|
||||
];
|
||||
|
||||
export const blockedRootRules: ASTRule[] = [
|
||||
{ name: 'this', description: 'Global object access — in the outer AsyncFunction scope (non-strict) \'this\' is the host global object, with the same reach as globalThis.' },
|
||||
{ name: 'globalThis', description: 'Global object access — primary global object alias that exposes every host API that parameter masking is meant to hide.' },
|
||||
{ name: 'global', description: 'Global object access — Node.js alias for globalThis; dynamic access (e.g. global["req"+"uire"]) bypasses string-literal detection.' },
|
||||
{ name: 'window', description: 'Global object access — browser global alias; inside Electron it also reaches Node.js APIs via window.bridge and similar.' },
|
||||
{ name: 'self', description: 'Global object access — Web Worker / browser alias for globalThis; available in some Electron renderer contexts.' },
|
||||
{ name: 'frames', description: 'Global object access — browser alias for the window.frames collection; can be used to navigate to an unsandboxed global.' },
|
||||
{ name: 'process', description: 'Node.js internals access — exposes mainModule, env, and other Node.js internals not part of the supported scripting API.' },
|
||||
{ name: 'module', description: 'Module system bypass — Node.js module wrapper object; .require and .children expose the full module graph.' },
|
||||
{ name: 'exports', description: 'Module system bypass — Node.js module exports object; mutating it affects the live module cache.' },
|
||||
{ name: 'Buffer', description: 'Unsafe memory access — the Buffer global provides allocUnsafe(), which reads uninitialised memory.' },
|
||||
{ name: 'constructor', description: 'Function constructor escape — in AsyncFunction scope this IS AsyncFunction; a direct call constructs a new function in the real global scope.' },
|
||||
{ name: 'arguments', description: 'Caller inspection — can leak the caller\'s frame in generator or sloppy-mode contexts, exposing host objects.' },
|
||||
];
|
||||
|
||||
export interface ThreatRule {
|
||||
name: string; // unique rule id.
|
||||
description: string; // message detailing the block reason.
|
||||
maskName?: string; // identifier to mask in the script's function scope
|
||||
maskValue?: unknown; // value bound to `maskName`. (normally `undefined` or a interceptor function).
|
||||
buildMaskValue?: (violationCheck: (script: string) => void) => unknown; // Factory called at buildMaskScope() time. Receives checkSandboxViolations so interceptors can perform full static analysis on dynamic input (e.g. eval strings).
|
||||
}
|
||||
import type { ThreatRule } from './script-security-rules';
|
||||
export type { ASTRule, ThreatRule } from './script-security-rules';
|
||||
export { blockedPropertyRules, blockedRootRules, maskRules } from './script-security-rules';
|
||||
|
||||
// mask interceptor binding rules.
|
||||
export const interceptorRules: ThreatRule[] = [
|
||||
@@ -97,66 +51,8 @@ export const interceptorRules: ThreatRule[] = [
|
||||
invariant(script && typeof script === 'string', 'eval is called with invalid or empty value');
|
||||
violationCheck(script);
|
||||
|
||||
|
||||
|
||||
return (0, eval)(script);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Runtime masks — bindings replaced with undefined to make them unreachable.
|
||||
export const maskRules: ThreatRule[] = [
|
||||
{
|
||||
name: 'globalThis',
|
||||
description: 'Prevents access to the globalThis object to prevent exposure of process, require, and other host APIs that parameter masking is meant to hide.',
|
||||
maskName: 'globalThis',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'global',
|
||||
description: 'Prevents access to the global parameter (Node.js alias for globalThis) to prevent dynamic access to host APIs (e.g. global["req"+"uire"]).',
|
||||
maskName: 'global',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'Function',
|
||||
description: 'Prevents access to the Function constructor to prevent creation of new functions in the real global scope, escaping parameter-level masking (e.g. Function("return process")()).',
|
||||
maskName: 'Function',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'process',
|
||||
description: 'Prevents access to the process object to prevent exposure of mainModule, env, and other Node.js internals not part of the supported scripting API.',
|
||||
maskName: 'process',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'setImmediate',
|
||||
description: 'Prevents access to the setImmediate function to prevent its use as an untracked async scheduling side-channel.',
|
||||
maskName: 'setImmediate',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'queueMicrotask',
|
||||
maskName: 'queueMicrotask',
|
||||
description: 'Prevents access to the queueMicrotask function to prevent scheduling work outside the async/await flow tracked by the executor, which would make clean shutdown harder.',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'Proxy',
|
||||
description: 'Prevents access to the Proxy constructor to prevent apply/construct traps from receiving unwrapped host objects, which enables prototype chain traversal to real host globals (CVE-2023-32314).',
|
||||
maskName: 'Proxy',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'Reflect',
|
||||
description: 'Prevents access to the Reflect object to prevent Reflect.apply() and Reflect.construct() from invoking functions with an explicit this value, bypassing the strict-mode this===undefined invariant.',
|
||||
maskName: 'Reflect',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'WebAssembly',
|
||||
description: 'Prevents access to the WebAssembly API to prevent loading and executing arbitrary native bytecode, which would bypass JS-level sandboxing entirely.',
|
||||
maskName: 'WebAssembly',
|
||||
maskValue: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
export interface ASTRule {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ThreatRule {
|
||||
name: string;
|
||||
description: string;
|
||||
maskName?: string;
|
||||
maskValue?: unknown;
|
||||
buildMaskValue?: (violationCheck: (script: string) => void) => unknown;
|
||||
}
|
||||
|
||||
export const blockedPropertyRules: ASTRule[] = [
|
||||
{ name: 'prototype', description: 'Prototype mutation — direct assignment (e.g. Promise.prototype.then = ...) can corrupt built-ins for all code in the sandbox.' },
|
||||
{ name: 'mainModule', description: 'Prevents accessing the reference property to the top-level module object.' },
|
||||
{ name: 'constructor', description: 'Prevents accessing .constructor on any object.' },
|
||||
{ name: '__proto__', description: 'Prototype mutation — direct prototype chain manipulation; can reassign an object\'s prototype to a host object.' },
|
||||
{ name: 'prepareStackTrace', description: 'Stack inspection escape — V8 stack trace hook (CVE-2023-29017, CVE-2023-30547); a crafted Error can run arbitrary code during stringify.' },
|
||||
{ name: 'captureStackTrace', description: 'Stack inspection — V8 method that captures the current call stack onto an object, exposing stack frame host objects.' },
|
||||
{ name: 'getPrototypeOf', description: 'Prototype chain traversal — can reach the .constructor of a host object and reconstruct Function.' },
|
||||
{ name: 'setPrototypeOf', description: 'Prototype mutation — directly replaces an object\'s prototype, enabling prototype chain manipulation at runtime.' },
|
||||
{ name: 'getFunction', description: 'Stack inspection — V8 CallSite method that leaks unsanitised host objects from the call stack.' },
|
||||
{ name: 'getThis', description: 'Stack inspection — V8 CallSite method that leaks the unsanitised receiver of each stack frame.' },
|
||||
{ name: '__defineGetter__', description: 'Accessor helper — deprecated method that bypasses the normal property descriptor flow.' },
|
||||
{ name: '__defineSetter__', description: 'Accessor helper — deprecated method that bypasses the normal property descriptor flow.' },
|
||||
{ name: '__lookupGetter__', description: 'Accessor helper — deprecated method that can be used to inspect hidden property descriptors.' },
|
||||
{ name: '__lookupSetter__', description: 'Accessor helper — deprecated method that can be used to inspect hidden property descriptors.' },
|
||||
{ name: 'defineProperty', description: 'Property descriptor manipulation — installs arbitrary getters, setters, or non-configurable properties on any object including built-ins.' },
|
||||
{ name: 'defineProperties', description: 'Property descriptor manipulation — same as defineProperty but for multiple properties at once.' },
|
||||
{ name: 'getOwnPropertyDescriptor', description: 'Property descriptor inspection — returns the full descriptor including any getter/setter functions, which may be host objects.' },
|
||||
{ name: 'getOwnPropertyDescriptors', description: 'Property descriptor inspection — returns all property descriptors at once; same risk as getOwnPropertyDescriptor.' },
|
||||
];
|
||||
|
||||
export const blockedRootRules: ASTRule[] = [
|
||||
{ name: 'this', description: 'Global object access — in the outer AsyncFunction scope (non-strict) \'this\' is the host global object, with the same reach as globalThis.' },
|
||||
{ name: 'globalThis', description: 'Global object access — primary global object alias that exposes every host API that parameter masking is meant to hide.' },
|
||||
{ name: 'global', description: 'Global object access — Node.js alias for globalThis; dynamic access (e.g. global["req"+"uire"]) bypasses string-literal detection.' },
|
||||
{ name: 'window', description: 'Global object access — browser global alias; inside Electron it also reaches Node.js APIs via window.bridge and similar.' },
|
||||
{ name: 'self', description: 'Global object access — Web Worker / browser alias for globalThis; available in some Electron renderer contexts.' },
|
||||
{ name: 'frames', description: 'Global object access — browser alias for the window.frames collection; can be used to navigate to an unsandboxed global.' },
|
||||
{ name: 'process', description: 'Node.js internals access — exposes mainModule, env, and other Node.js internals not part of the supported scripting API.' },
|
||||
{ name: 'module', description: 'Module system bypass — Node.js module wrapper object; .require and .children expose the full module graph.' },
|
||||
{ name: 'exports', description: 'Module system bypass — Node.js module exports object; mutating it affects the live module cache.' },
|
||||
{ name: 'Buffer', description: 'Unsafe memory access — the Buffer global provides allocUnsafe(), which reads uninitialised memory.' },
|
||||
{ name: 'constructor', description: 'Function constructor escape — in AsyncFunction scope this IS AsyncFunction; a direct call constructs a new function in the real global scope.' },
|
||||
{ name: 'arguments', description: 'Caller inspection — can leak the caller\'s frame in generator or sloppy-mode contexts, exposing host objects.' },
|
||||
];
|
||||
|
||||
export const maskRules: ThreatRule[] = [
|
||||
{
|
||||
name: 'globalThis',
|
||||
description: 'Prevents access to the globalThis object to prevent exposure of process, require, and other host APIs that parameter masking is meant to hide.',
|
||||
maskName: 'globalThis',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'global',
|
||||
description: 'Prevents access to the global parameter (Node.js alias for globalThis) to prevent dynamic access to host APIs (e.g. global["req"+"uire"]).',
|
||||
maskName: 'global',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'Function',
|
||||
description: 'Prevents access to the Function constructor to prevent creation of new functions in the real global scope, escaping parameter-level masking (e.g. Function("return process")()).',
|
||||
maskName: 'Function',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'process',
|
||||
description: 'Prevents access to the process object to prevent exposure of mainModule, env, and other Node.js internals not part of the supported scripting API.',
|
||||
maskName: 'process',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'setImmediate',
|
||||
description: 'Prevents access to the setImmediate function to prevent its use as an untracked async scheduling side-channel.',
|
||||
maskName: 'setImmediate',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'queueMicrotask',
|
||||
maskName: 'queueMicrotask',
|
||||
description: 'Prevents access to the queueMicrotask function to prevent scheduling work outside the async/await flow tracked by the executor, which would make clean shutdown harder.',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'Proxy',
|
||||
description: 'Prevents access to the Proxy constructor to prevent apply/construct traps from receiving unwrapped host objects, which enables prototype chain traversal to real host globals (CVE-2023-32314).',
|
||||
maskName: 'Proxy',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'Reflect',
|
||||
description: 'Prevents access to the Reflect object to prevent Reflect.apply() and Reflect.construct() from invoking functions with an explicit this value, bypassing the strict-mode this===undefined invariant.',
|
||||
maskName: 'Reflect',
|
||||
maskValue: undefined,
|
||||
},
|
||||
{
|
||||
name: 'WebAssembly',
|
||||
description: 'Prevents access to the WebAssembly API to prevent loading and executing arbitrary native bytecode, which would bypass JS-level sandboxing entirely.',
|
||||
maskName: 'WebAssembly',
|
||||
maskValue: undefined,
|
||||
},
|
||||
];
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { EditorFromTextArea, MarkerRange } from 'codemirror';
|
||||
|
||||
import { models, services } from '~/insomnia-data';
|
||||
import { decryptSecretValue } from '~/utils/vault';
|
||||
import { decryptSecretValue } from '~/utils/vault-crypto';
|
||||
|
||||
import type { NunjucksParsedTag, NunjucksParsedTagArg, RenderPurpose } from '../templating/types';
|
||||
import { decryptVaultKeyFromSession } from '../utils/vault';
|
||||
@@ -50,7 +50,6 @@ export function normalizeToDotAndBracketNotation(prefix: string) {
|
||||
return objectPath.normalize(prefix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a Liquid template tag string into a usable object
|
||||
* @param {string} tagStr - the template string for the tag
|
||||
@@ -159,10 +158,10 @@ export async function maskOrDecryptVaultDataIfNecessary(vaultEnvironmentData: an
|
||||
if (isVaultEnabled && vaultKey) {
|
||||
const symmetricKey = (await decryptVaultKeyFromSession(vaultKey, true)) as JsonWebKey;
|
||||
// decrypt all secret values under vaultEnvironmentPath property in context
|
||||
Object.keys(vaultEnvironmentData).forEach(vaultContextKey => {
|
||||
for (const vaultContextKey of Object.keys(vaultEnvironmentData)) {
|
||||
const encryptedValue = vaultEnvironmentData[vaultContextKey];
|
||||
vaultEnvironmentData[vaultContextKey] = decryptSecretValue(encryptedValue, symmetricKey);
|
||||
});
|
||||
vaultEnvironmentData[vaultContextKey] = await decryptSecretValue(encryptedValue, symmetricKey);
|
||||
}
|
||||
} else if (isVaultEnabled && !vaultKey) {
|
||||
// remove all values under vaultEnvironmentPath if no vault key found
|
||||
vaultEnvironmentData = {};
|
||||
|
||||
@@ -148,9 +148,10 @@ export const RequestActionsDropdown = ({
|
||||
const copyAsCurl = async () => {
|
||||
try {
|
||||
const har = await exportHarRequest(request._id, workspaceId);
|
||||
const { HTTPSnippet } = await import('httpsnippet');
|
||||
const snippet = new HTTPSnippet(har);
|
||||
const cmd = snippet.convert('shell', 'curl');
|
||||
if (!har) {
|
||||
return;
|
||||
}
|
||||
const cmd = await window.main.generateCodeSnippet({ har, target: 'shell', client: 'curl' });
|
||||
|
||||
if (cmd) {
|
||||
window.clipboard.writeText(cmd);
|
||||
|
||||
+28
-7
@@ -20,7 +20,7 @@ import { checkNestedKeys, ensureKeyIsValid } from '~/utils/environment-utils';
|
||||
|
||||
import { generateId } from '../../../../common/misc';
|
||||
import { base64decode } from '../../../../utils/vault';
|
||||
import { decryptSecretValue, encryptSecretValue } from '../../../../utils/vault';
|
||||
import { decryptSecretValue, encryptSecretValue } from '../../../../utils/vault-crypto';
|
||||
import { PromptButton } from '../../base/prompt-button';
|
||||
import { Icon } from '../../icon';
|
||||
import { showModal } from '../../modals';
|
||||
@@ -77,8 +77,29 @@ export const EnvironmentKVEditor = ({
|
||||
);
|
||||
const codeModalRef = useRef<CodePromptModalHandle>(null);
|
||||
const [kvPairError, setKvPairError] = useState<{ id: string; error: string }[]>([]);
|
||||
const [decryptedValues, setDecryptedValues] = useState<Record<string, string>>({});
|
||||
const symmetricKey = vaultKey === '' ? {} : base64decode(vaultKey, true);
|
||||
|
||||
useEffect(() => {
|
||||
const secretPairs = kvPairs.filter(p => p.type === EnvironmentKvPairDataType.SECRET);
|
||||
if (secretPairs.length === 0 || Object.keys(symmetricKey).length === 0) {
|
||||
setDecryptedValues({});
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
Promise.all(
|
||||
secretPairs.map(async p => ({ id: p.id, value: await decryptSecretValue(p.value, symmetricKey as JsonWebKey) })),
|
||||
)
|
||||
.then(results => {
|
||||
if (!cancelled) {
|
||||
setDecryptedValues(Object.fromEntries(results.map(r => [r.id, r.value])));
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
return () => { cancelled = true; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(kvPairs.filter(p => p.type === EnvironmentKvPairDataType.SECRET).map(p => ({ id: p.id, value: p.value }))), vaultKey]);
|
||||
|
||||
const commonItemTypes = [
|
||||
{
|
||||
id: EnvironmentKvPairDataType.STRING,
|
||||
@@ -152,7 +173,7 @@ export const EnvironmentKVEditor = ({
|
||||
onChange(kvPairs);
|
||||
};
|
||||
|
||||
const handleItemTypeChange = (id: string, newType: EnvironmentKvPairDataType) => {
|
||||
const handleItemTypeChange = async (id: string, newType: EnvironmentKvPairDataType) => {
|
||||
const targetItem = kvPairs.find(pair => pair.id === id);
|
||||
if (targetItem) {
|
||||
const { type: originType, value: originValue } = targetItem;
|
||||
@@ -172,13 +193,13 @@ export const EnvironmentKVEditor = ({
|
||||
if (yes) {
|
||||
handleItemChange(id, 'type', newType);
|
||||
// decrypt and save the value
|
||||
handleItemChange(id, 'value', decryptSecretValue(originValue, symmetricKey));
|
||||
handleItemChange(id, 'value', await decryptSecretValue(originValue, symmetricKey as JsonWebKey));
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (newType === EnvironmentKvPairDataType.SECRET) {
|
||||
// encrypt value if set to secret type
|
||||
handleItemChange(id, 'value', encryptSecretValue(originValue, symmetricKey));
|
||||
handleItemChange(id, 'value', await encryptSecretValue(originValue, symmetricKey as JsonWebKey));
|
||||
handleItemChange(id, 'type', newType);
|
||||
} else {
|
||||
handleItemChange(id, 'type', newType);
|
||||
@@ -310,9 +331,9 @@ export const EnvironmentKVEditor = ({
|
||||
itemId={id}
|
||||
enabled={enabled && !disabled}
|
||||
placeholder="Input Secret"
|
||||
value={decryptSecretValue(value, symmetricKey)}
|
||||
onChange={newValue => {
|
||||
const encryptedValue = encryptSecretValue(newValue, symmetricKey);
|
||||
value={decryptedValues[id] ?? ''}
|
||||
onChange={async newValue => {
|
||||
const encryptedValue = await encryptSecretValue(newValue, symmetricKey as JsonWebKey);
|
||||
handleItemChange(id, 'value', encryptedValue);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HTTPSnippetClient, HTTPSnippetTarget } from 'httpsnippet';
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
|
||||
import type { Request } from '~/insomnia-data';
|
||||
@@ -65,11 +65,13 @@ export const GenerateCodeModal = forwardRef<GenerateCodeModalHandle, Props>((pro
|
||||
|
||||
const [snippet, setSnippet] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current?.setValue(snippet);
|
||||
}, [snippet]);
|
||||
|
||||
const generateCode = useCallback(
|
||||
async (request: Request, target?: HTTPSnippetTarget, client?: HTTPSnippetClient) => {
|
||||
const { HTTPSnippet, availableTargets } = await import('httpsnippet');
|
||||
|
||||
const targets = availableTargets();
|
||||
const targets = (await window.main.getCodeSnippetTargets()) as HTTPSnippetTarget[];
|
||||
const targetOrFallback = target || (targets.find(t => t.key === 'shell') as HTTPSnippetTarget);
|
||||
const clientOrFallback = client || (targetOrFallback.clients.find(t => t.key === 'curl') as HTTPSnippetClient);
|
||||
|
||||
@@ -89,9 +91,12 @@ export const GenerateCodeModal = forwardRef<GenerateCodeModalHandle, Props>((pro
|
||||
);
|
||||
const har = await exportHarWithRequest(request, props.environmentId, addContentLength);
|
||||
if (har) {
|
||||
const snippet = new HTTPSnippet(har);
|
||||
const cmd = snippet.convert(targetOrFallback.key, clientOrFallback.key) || '';
|
||||
setSnippet(cmd);
|
||||
const cmd = await window.main.generateCodeSnippet({
|
||||
har,
|
||||
target: targetOrFallback.key,
|
||||
client: clientOrFallback.key,
|
||||
});
|
||||
setSnippet(cmd as string);
|
||||
}
|
||||
|
||||
window.main.trackAnalyticsEvent({
|
||||
@@ -189,7 +194,6 @@ export const GenerateCodeModal = forwardRef<GenerateCodeModalHandle, Props>((pro
|
||||
id="generate-code-modal-content"
|
||||
placeholder="Generating code snippet..."
|
||||
className="border-top"
|
||||
key={Date.now()}
|
||||
mode={MODE_MAP[target.key] || target.key}
|
||||
ref={editorRef}
|
||||
defaultValue={snippet}
|
||||
|
||||
@@ -554,7 +554,8 @@ export const WorkspaceEnvironmentsEditModal = ({ onClose }: { onClose: () => voi
|
||||
</div>
|
||||
<Button
|
||||
onPress={close}
|
||||
className="rounded-xs border border-solid border-(--hl-md) px-3 py-2 text-(--color-font) transition-colors hover:no-underline"
|
||||
isDisabled={updateEnvironmentFetcher.state !== 'idle'}
|
||||
className="rounded-xs border border-solid border-(--hl-md) px-3 py-2 text-(--color-font) transition-colors hover:no-underline aria-disabled:opacity-50"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Switch } from 'react-aria-components';
|
||||
|
||||
import { useRootLoaderData } from '~/root';
|
||||
|
||||
import { type ASTRule, blockedPropertyRules, blockedRootRules, maskRules, type ThreatRule } from '../../../scripting/script-security-policy';
|
||||
import { type ASTRule, blockedPropertyRules, blockedRootRules, maskRules, type ThreatRule } from '../../../scripting/script-security-rules';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
|
||||
const DISABLED_TOOLTIP = 'Enable the script sandbox to configure individual rules';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { status } from '@grpc/grpc-js';
|
||||
import classnames from 'classnames';
|
||||
import React, { type FC, memo } from 'react';
|
||||
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
const GRPC_STATUS_OK = 0;
|
||||
|
||||
interface Props {
|
||||
statusCode?: number;
|
||||
small?: boolean;
|
||||
@@ -12,8 +13,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const GrpcStatusTag: FC<Props> = memo(({ statusMessage, statusCode, small, tooltipDelay }) => {
|
||||
const colorClass = statusCode === status.OK ? 'bg-success' : 'bg-danger';
|
||||
const message = statusCode === status.OK ? 'OK' : statusMessage;
|
||||
const colorClass = statusCode === GRPC_STATUS_OK ? 'bg-success' : 'bg-danger';
|
||||
const message = statusCode === GRPC_STATUS_OK ? 'OK' : statusMessage;
|
||||
return (
|
||||
<div
|
||||
className={classnames('tag', colorClass, {
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import React, { type FC, useState } from 'react';
|
||||
import { Cookie } from 'tough-cookie';
|
||||
|
||||
import { AnalyticsEvent } from '~/ui/analytics';
|
||||
import { CookiesModal } from '~/ui/components/modals/cookies-modal';
|
||||
|
||||
const parseSetCookieHeader = (headerValue: string) => {
|
||||
const [nameValue = ''] = headerValue.split(';');
|
||||
const separatorIndex = nameValue.indexOf('=');
|
||||
|
||||
if (separatorIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: nameValue.slice(0, separatorIndex).trim(),
|
||||
value: nameValue.slice(separatorIndex + 1).trim(),
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
cookiesSent?: boolean | null;
|
||||
cookiesStored?: boolean | null;
|
||||
@@ -13,10 +26,10 @@ interface Props {
|
||||
export const ResponseCookiesViewer: FC<Props> = props => {
|
||||
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
|
||||
const renderRow = (h: any, i: number) => {
|
||||
let cookie: Cookie | undefined | null = null;
|
||||
let cookie: ReturnType<typeof parseSetCookieHeader> = null;
|
||||
|
||||
try {
|
||||
cookie = h ? Cookie.parse(h.value || '', { loose: true }) : null;
|
||||
cookie = h ? parseSetCookieHeader(h.value || '') : null;
|
||||
} catch {
|
||||
console.warn('Failed to parse set-cookie header', h);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ResponseMultipartViewer: FC<Props> = ({
|
||||
return;
|
||||
}
|
||||
const contentType = getContentTypeFromHeaders(selectedPart.headers, 'text/plain');
|
||||
const extension = mimeExtension(contentType) || '.txt';
|
||||
const extension = mimeExtension(contentType) || 'txt';
|
||||
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
|
||||
const dir = lastDir || window.app.getPath('desktop');
|
||||
const date = format(Date.now(), 'yyyy-MM-dd');
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import iconv from 'iconv-lite';
|
||||
import { Fragment, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW } from '~/insomnia-data/common';
|
||||
@@ -14,6 +13,25 @@ import { ResponseMultipartViewer } from './response-multipart-viewer';
|
||||
import { ResponsePDFViewer } from './response-pdf-viewer';
|
||||
import { ResponseWebView } from './response-web-view';
|
||||
|
||||
const CHARSET_ALIASES: Record<string, string> = {
|
||||
utf8: 'utf8',
|
||||
utf16le: 'utf-16le',
|
||||
ucs2: 'utf-16le',
|
||||
'ucs-2': 'utf-16le',
|
||||
latin1: 'iso-8859-1',
|
||||
binary: 'iso-8859-1',
|
||||
ascii: 'ascii',
|
||||
win1250: 'windows-1250',
|
||||
win1251: 'windows-1251',
|
||||
win1252: 'windows-1252',
|
||||
win1253: 'windows-1253',
|
||||
win1254: 'windows-1254',
|
||||
win1255: 'windows-1255',
|
||||
win1256: 'windows-1256',
|
||||
win1257: 'windows-1257',
|
||||
win1258: 'windows-1258',
|
||||
};
|
||||
|
||||
let alwaysShowLargeResponses = false;
|
||||
|
||||
export interface ResponseViewerHandle {
|
||||
@@ -148,9 +166,10 @@ export const ResponseViewer = ({
|
||||
// Show everything else as "source"
|
||||
const match = _getContentType().match(/charset=([\w-]+)/);
|
||||
const charset = match && match.length >= 2 ? match[1] : 'utf8';
|
||||
// Sometimes iconv conversion fails so fallback to regular buffer
|
||||
const label = CHARSET_ALIASES[charset.toLowerCase()] ?? charset;
|
||||
// Sometimes decoding fails so fallback to regular buffer
|
||||
try {
|
||||
return iconv.decode(overSizedBody, charset);
|
||||
return new TextDecoder(label).decode(overSizedBody);
|
||||
} catch (err) {
|
||||
console.warn('[response] Failed to decode body', err);
|
||||
return overSizedBody.toString();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { decryptSecretValue, encryptSecretValue } from './vault-crypto';
|
||||
|
||||
const mockEncrypt = vi.fn();
|
||||
const mockDecrypt = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
Object.defineProperty(window, 'main', {
|
||||
value: { vault: { encryptSecretValue: mockEncrypt, decryptSecretValue: mockDecrypt } },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
const VALID_KEY: JsonWebKey = {
|
||||
kty: 'oct',
|
||||
alg: 'A256GCM',
|
||||
ext: true,
|
||||
key_ops: ['encrypt', 'decrypt'],
|
||||
k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE',
|
||||
};
|
||||
|
||||
describe('encryptSecretValue', () => {
|
||||
it('returns rawValue when symmetricKey is not an object', async () => {
|
||||
expect(await encryptSecretValue('secret', 'invalid' as unknown as JsonWebKey)).toBe('secret');
|
||||
expect(mockEncrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns rawValue when symmetricKey is empty object', async () => {
|
||||
expect(await encryptSecretValue('secret', {})).toBe('secret');
|
||||
expect(mockEncrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('delegates to window.main.vault.encryptSecretValue with a valid key', async () => {
|
||||
mockEncrypt.mockResolvedValue('encrypted-value');
|
||||
const result = await encryptSecretValue('my secret', VALID_KEY);
|
||||
expect(mockEncrypt).toHaveBeenCalledWith('my secret', VALID_KEY);
|
||||
expect(result).toBe('encrypted-value');
|
||||
});
|
||||
|
||||
it('returns rawValue when IPC call throws', async () => {
|
||||
mockEncrypt.mockRejectedValue(new Error('IPC error'));
|
||||
const result = await encryptSecretValue('my secret', VALID_KEY);
|
||||
expect(result).toBe('my secret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptSecretValue', () => {
|
||||
it('returns encryptedValue when symmetricKey is not an object', async () => {
|
||||
expect(await decryptSecretValue('encrypted', 'invalid' as unknown as JsonWebKey)).toBe('encrypted');
|
||||
expect(mockDecrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns encryptedValue when symmetricKey is empty object', async () => {
|
||||
expect(await decryptSecretValue('encrypted', {})).toBe('encrypted');
|
||||
expect(mockDecrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('delegates to window.main.vault.decryptSecretValue with a valid key', async () => {
|
||||
mockDecrypt.mockResolvedValue('plaintext');
|
||||
const result = await decryptSecretValue('encrypted-blob', VALID_KEY);
|
||||
expect(mockDecrypt).toHaveBeenCalledWith('encrypted-blob', VALID_KEY);
|
||||
expect(result).toBe('plaintext');
|
||||
});
|
||||
|
||||
it('returns encryptedValue when IPC call throws', async () => {
|
||||
mockDecrypt.mockRejectedValue(new Error('IPC error'));
|
||||
const result = await decryptSecretValue('encrypted-blob', VALID_KEY);
|
||||
expect(result).toBe('encrypted-blob');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
export const encryptSecretValue = async (rawValue: string, symmetricKey: JsonWebKey): Promise<string> => {
|
||||
if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) {
|
||||
return rawValue;
|
||||
}
|
||||
try {
|
||||
return await window.main.vault.encryptSecretValue(rawValue, symmetricKey);
|
||||
} catch {
|
||||
return rawValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const decryptSecretValue = async (encryptedValue: string, symmetricKey: JsonWebKey): Promise<string> => {
|
||||
if (typeof symmetricKey !== 'object' || Object.keys(symmetricKey).length === 0) {
|
||||
return encryptedValue;
|
||||
}
|
||||
try {
|
||||
return await window.main.vault.decryptSecretValue(encryptedValue, symmetricKey);
|
||||
} catch {
|
||||
return encryptedValue;
|
||||
}
|
||||
};
|
||||
Vendored
+28
@@ -5,8 +5,36 @@ import type { DatabaseBridgeAPI } from '../src/main/ipc/database';
|
||||
import type { DiffMatchPatch, DiffOp } from 'diff-match-patch-ts';
|
||||
import type { Services } from '~/insomnia-data';
|
||||
|
||||
type RendererEnv = {
|
||||
INSOMNIA_GITLAB_REDIRECT_URI: string | undefined;
|
||||
INSOMNIA_GITLAB_CLIENT_ID: string | undefined;
|
||||
INSOMNIA_GITLAB_API_URL: string | undefined;
|
||||
PLAYWRIGHT_TEST: string | undefined;
|
||||
INSOMNIA_SKIP_ONBOARDING: string | undefined;
|
||||
INSOMNIA_SESSION: string | undefined;
|
||||
INSOMNIA_SECRET_KEY: string | undefined;
|
||||
INSOMNIA_PUBLIC_KEY: string | undefined;
|
||||
INSOMNIA_VAULT_SALT: string | undefined;
|
||||
INSOMNIA_VAULT_KEY: string | undefined;
|
||||
INSOMNIA_VAULT_SRP_SECRET: string | undefined;
|
||||
INSOMNIA_ENV: string | undefined;
|
||||
BUILD_DATE: string | undefined;
|
||||
PORTABLE_EXECUTABLE_DIR: string | undefined;
|
||||
OAUTH_REDIRECT_URL: string | undefined;
|
||||
OAUTH_RELAY_URL: string | undefined;
|
||||
INSOMNIA_API_URL: string | undefined;
|
||||
INSOMNIA_MOCK_API_URL: string | undefined;
|
||||
INSOMNIA_AI_URL: string | undefined;
|
||||
KONNECT_API_URL: string | undefined;
|
||||
INSOMNIA_APP_WEBSITE_URL: string | undefined;
|
||||
INSOMNIA_GITHUB_REST_API_URL: string | undefined;
|
||||
INSOMNIA_GITHUB_API_URL: string | undefined;
|
||||
INSOMNIA_ENABLE_PLUGIN_BRIDGE: string | undefined;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
env: RendererEnv;
|
||||
main: RendererToMainBridgeAPI;
|
||||
bridge: HiddenBrowserWindowToMainBridgeAPI;
|
||||
database: DatabaseBridgeAPI;
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
declare module 'node-forge/lib/forge' {
|
||||
import forge from 'node-forge';
|
||||
|
||||
export default forge;
|
||||
}
|
||||
|
||||
declare module 'node-forge/lib/util';
|
||||
declare module 'node-forge/lib/cipher';
|
||||
declare module 'node-forge/lib/cipherModes';
|
||||
declare module 'node-forge/lib/aes';
|
||||
@@ -56,6 +56,8 @@ export default defineConfig(({ mode }) => {
|
||||
// builds inline the module directly (avoids runtime require() in server bundle).
|
||||
'~/network/network-adapter': path.resolve(__dirname, './src/network/network-adapter.renderer'),
|
||||
'~': path.resolve(__dirname, './src'),
|
||||
// Shim Node's `path` module for browser-safe dependencies (e.g. mime-types uses path.extname).
|
||||
'path': path.resolve(__dirname, './src/path-shim.ts'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -66,7 +68,7 @@ export default defineConfig(({ mode }) => {
|
||||
modules: [
|
||||
'electron',
|
||||
...externalDependencies,
|
||||
...builtinModules.filter(m => m !== 'buffer'),
|
||||
...builtinModules.filter(m => m !== 'buffer' && m !== 'path'),
|
||||
...builtinModules.map(m => `node:${m}`),
|
||||
],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user