securely wrap execution (#8451)

* test using a wrapper that sets security context before invoking the main application

* add build step to pr ci

* try with diff ver string

* statically link the standard libraries for portability

* more static linking

* prior version string

* remove pr substring from version

* try using a different version string
This commit is contained in:
Ryan Willis
2025-03-25 08:57:53 -07:00
committed by GitHub
parent 32c686f672
commit 05d15b4abf
11 changed files with 433 additions and 3 deletions

View File

@@ -104,6 +104,14 @@ jobs:
shell: bash
run: NODE_OPTIONS='--max_old_space_size=6144' npm run package:windows:unpacked -w insomnia
# wraps the Insomnia.exe PE with another PE that will bootstrap particular security
# features added to Windows 8 in order to stopgap CVE-2025-1353
- name: Compile secure wrapper (Windows only)
if: runner.os == 'Windows'
shell: bash
run: ./build-secure-wrapper.sh CI
- name: Move .dll and .exe files to /tosign (PowerShell)
if: runner.os == 'Windows'
shell: pwsh

View File

@@ -50,12 +50,17 @@ jobs:
- name: Bump version
shell: bash
run: npm --workspaces version prerelease --preid="$(git rev-parse --short HEAD)${{ github.event_name == 'pull_request' && '.pr-$PR_NUMBER' || '' }}" --no-git-tag-version
run: npm --workspaces version prerelease --preid="alpha-pr-$(git rev-parse --short HEAD)" --no-git-tag-version
- name: Package
shell: bash
run: NODE_OPTIONS='--max_old_space_size=6144' BUILD_TARGETS='${{ matrix.build-targets }}' npm run app-package
- name: Verify secure wrapper (Windows)
if: ${{ matrix.os == 'windows-latest' }}
shell: bash
run: NODE_OPTIONS='--max_old_space_size=6144' ./build-secure-wrapper.sh
# See https://github.com/electron/electron/issues/42510#issuecomment-2171583086
- if: ${{ runner.os == 'Linux' }}
name: Lift unprivileged user namespace restrictions

5
.gitignore vendored
View File

@@ -35,3 +35,8 @@ dist
.history
*.node
rootCA2.*
*.exe
*.o
final.cpp
insomnia.ico
final.rc

View File

@@ -41,4 +41,7 @@
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[cpp]": {
"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"
},
}

54
build-secure-wrapper.sh Normal file
View File

@@ -0,0 +1,54 @@
set -e
VERSION=$(jq .version ./packages/insomnia/package.json -rj)
echo "Starting Insomnia secure wrapper build for version $VERSION..."
MAJOR=$(echo $VERSION | cut -d '.' -f 1)
MINOR=$(echo $VERSION | cut -d '.' -f 2)
PATCH=$(echo $VERSION | cut -d '.' -f 3 | cut -d '-' -f 1)
TAG=$(echo $VERSION | cut -d '-' -f 2)
SRC_DIR=packages/insomnia/src
CPP_DIR=$SRC_DIR/cpp
DEST_DIR=packages/insomnia/dist/win-unpacked
if [ -n "$TAG" ]; then
TAG="-$TAG"
fi
# if an arg is passed, skip the build step (CI)
if [ ! $1 ]; then
echo "Building Insomnia electron application..."
npm run package:windows:unpacked -w insomnia
fi
cp $DEST_DIR/Insomnia.exe $DEST_DIR/insomnia.dll
cp $SRC_DIR/icons/icon.ico $CPP_DIR/insomnia.ico
echo "Injecting version strings..."
sed "s/__VERSION__/$VERSION/g" $CPP_DIR/insomnia.cpp > $CPP_DIR/final.cpp
sed "s/__MAJOR__/$MAJOR/g" $CPP_DIR/resources.rc > $CPP_DIR/final.rc
sed -i "s/__MINOR__/$MINOR/g" $CPP_DIR/final.rc
sed -i "s/__PATCH__/$PATCH/g" $CPP_DIR/final.rc
sed -i "s/__TAG__/$TAG/g" $CPP_DIR/final.rc
sed -i "s/__YEAR__/$(date +%Y)/g" $CPP_DIR/final.rc
echo "Compiling resources..."
windres $CPP_DIR/final.rc $CPP_DIR/res.o
echo "Compiling Insomnia..."
g++ -lkernel32 -mwindows -c $CPP_DIR/final.cpp -o $CPP_DIR/insomnia.o
echo "Linking Insomnia..."
g++ -O2 -static -static-libgcc -static-libstdc++ -mwindows -lwinpthread $CPP_DIR/insomnia.o $CPP_DIR/res.o -o $DEST_DIR/Insomnia.exe
echo "Secure wapper built successfully."
if [ ! $1 ]; then
echo "Packaging distributables..."
npm run package:windows:dist -w insomnia
echo "Resetting to prevent accidental loops..."
mv $DEST_DIR/insomnia.dll $DEST_DIR/Insomnia.exe
fi
echo "Done."

View File

@@ -0,0 +1,279 @@
// NOTE: The calls in this wrapper are only supported on Windows >= 8.
#define _WIN32_WINNT 0x602
#define __INSOMNIA_OUTPUT_BUFFER_SIZE 8192
#include <cstdio>
#include <cstring>
#include <io.h>
#include <string>
#include <windows.h>
// #define DEBUG
const char *INSOMNIA_VERSION = "__VERSION__";
const char *INSOMNIA_ISSUE_REPORT_PREFIX =
"\n\nPlease report this issue on GitHub:\n";
const char *INSOMNIA_ISSUE_URL = "https://github.com/Kong/insomnia/issues";
const char *INSOMNIA_ISSUE_REPORT_POSTFIX =
"\nWould you like to open the issue report URL in your default browser?";
const char *SQUIRREL_INSTALL = "--squirrel-install";
const char *SQUIRREL_UPDATED = "--squirrel-updated";
const char *SQUIRREL_OBSOLETE = "--squirrel-obsolete";
const char *SQUIRREL_UNINSTALL = "--squirrel-uninstall";
const char *SQUIRREL_FIRST_RUN = "--squirrel-first-run";
#ifdef DEBUG
HANDLE hDebugLog;
BOOL handleCreated = FALSE;
void DebugLog(const char *msg) {
if (handleCreated) {
::WriteFile(hDebugLog, msg, strlen(msg), NULL, NULL);
::WriteFile(hDebugLog, "\n", 1, NULL, NULL);
}
}
#endif
int ExitWithWarning(int cmdShow, const char *msg) {
std::string finalMsg(msg);
finalMsg += INSOMNIA_ISSUE_REPORT_POSTFIX;
finalMsg += INSOMNIA_ISSUE_URL;
finalMsg += INSOMNIA_ISSUE_REPORT_POSTFIX;
if (::MessageBox(NULL, finalMsg.c_str(),
"Insomnia was unable to start up properly",
MB_YESNO | MB_ICONERROR) == IDYES) {
// Open the issue report URL in the default browser
::ShellExecute(0, 0, INSOMNIA_ISSUE_URL, NULL, NULL, cmdShow);
}
#ifdef DEBUG
if (handleCreated) {
::CloseHandle(hDebugLog);
}
#endif
return 1;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {
#ifdef DEBUG
char temporaryPath[MAX_PATH];
::GetTempPath(MAX_PATH, temporaryPath);
std::string tempPath(temporaryPath);
tempPath.append("insomnia.log");
hDebugLog = ::CreateFile(tempPath.c_str(), FILE_APPEND_DATA, FILE_SHARE_WRITE,
NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDebugLog == INVALID_HANDLE_VALUE) {
return ::ExitWithWarning(nCmdShow, "Could not create debug log file.");
}
handleCreated = TRUE;
DebugLog("__________________________________________________");
DebugLog(lpCmdLine);
#endif
char insomniaExecutable[MAX_PATH];
::GetModuleFileName(NULL, insomniaExecutable, sizeof(insomniaExecutable));
std::string currentPath(insomniaExecutable);
currentPath = currentPath.substr(0, currentPath.find_last_of("\\/"));
// get one directory above
std::string updatePath(currentPath);
updatePath = updatePath.substr(0, updatePath.find_last_of("\\/"));
updatePath.append("\\Update.exe");
// preserve the console output from the original executable
::AttachConsole(-1);
::WriteConsole(::GetStdHandle(STD_OUTPUT_HANDLE), "Insomnia is starting...\n",
25, NULL, NULL);
::WriteConsole(::GetStdHandle(STD_OUTPUT_HANDLE), lpCmdLine,
strlen(lpCmdLine), NULL, NULL);
::WriteConsole(::GetStdHandle(STD_OUTPUT_HANDLE), "\n", 1, NULL, NULL);
if (strncmp(lpCmdLine, SQUIRREL_INSTALL, strlen(SQUIRREL_INSTALL)) == 0) {
#ifdef DEBUG
::DebugLog("Squirrel.Windows install");
#endif
// Squirrel.Windows install
std::string args = "--createShortcut=";
args.append(insomniaExecutable);
::ShellExecute(0, "open", updatePath.c_str(), args.c_str(), NULL, SW_HIDE);
return 0;
} else if (strncmp(lpCmdLine, SQUIRREL_UPDATED, strlen(SQUIRREL_UPDATED)) ==
0 ||
strncmp(lpCmdLine, SQUIRREL_OBSOLETE, strlen(SQUIRREL_OBSOLETE)) ==
0) {
#ifdef DEBUG
::DebugLog("Squirrel.Windows updated or obsoleted");
#endif
// Squirrel.Windows update
return 0;
} else if (strncmp(lpCmdLine, SQUIRREL_UNINSTALL,
strlen(SQUIRREL_UNINSTALL)) == 0) {
// Squirrel.Windows uninstall
std::string args = "--removeShortcut=";
args.append(insomniaExecutable);
::ShellExecute(0, "open", updatePath.c_str(), args.c_str(), NULL, SW_HIDE);
#ifdef DEBUG
::DebugLog("Squirrel.Windows uninstall");
#endif
return 0;
} else if (strncmp(lpCmdLine, SQUIRREL_FIRST_RUN,
strlen(SQUIRREL_FIRST_RUN)) == 0) {
// Squirrel.Windows first run
#ifdef DEBUG
::DebugLog("Squirrel.Windows first run");
#endif
}
::PROCESS_MITIGATION_POLICY psp = ::ProcessSignaturePolicy;
::PROCESS_MITIGATION_POLICY pilp = ::ProcessImageLoadPolicy;
::PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY pmbsp;
::PROCESS_MITIGATION_IMAGE_LOAD_POLICY pmilp;
::PROCESS_INFORMATION pi;
::SECURITY_ATTRIBUTES sa;
::STARTUPINFO si;
::DWORD insomniaOutputBytesRead;
char insomniaOutputBuffer[__INSOMNIA_OUTPUT_BUFFER_SIZE];
if (!::GetProcessMitigationPolicy(::GetCurrentProcess(), psp, &pmbsp,
sizeof(pmbsp))) {
return ::ExitWithWarning(nCmdShow, "Could not get ProcessImageLoadPolicy.");
}
if (pmbsp.MitigationOptIn == 0) {
pmbsp.MitigationOptIn = 1;
if (!::SetProcessMitigationPolicy(psp, &pmbsp, sizeof(pmbsp))) {
return ::ExitWithWarning(nCmdShow,
"Could not set ProcessImageLoadPolicy.");
}
}
if (!::GetProcessMitigationPolicy(::GetCurrentProcess(), pilp, &pmilp,
sizeof(pmilp))) {
return ::ExitWithWarning(nCmdShow, "Could not get ProcessImageLoadPolicy.");
}
if (pmilp.PreferSystem32Images == 0) {
pmilp.PreferSystem32Images = 1;
if (!::SetProcessMitigationPolicy(pilp, &pmilp, sizeof(pmilp))) {
return ::ExitWithWarning(nCmdShow,
"Could not set ProcessImageLoadPolicy.");
}
}
::ZeroMemory(&pi, sizeof(pi));
::ZeroMemory(&si, sizeof(si));
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
HANDLE outrd, outwr;
if (!::CreatePipe(&outrd, &outwr, &sa, 0)) {
return ::ExitWithWarning(nCmdShow, "Could not create pipe.");
}
if (!::SetHandleInformation(outrd, HANDLE_FLAG_INHERIT, 0)) {
return ::ExitWithWarning(nCmdShow, "Could not set handle information.");
}
si.cb = sizeof(si);
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdOutput = outwr;
si.hStdError = outwr;
std::string sourceInsomniaExe(currentPath);
sourceInsomniaExe.append("\\insomnia.dll");
#ifdef DEBUG
::DebugLog("Current path:");
::DebugLog(currentPath.c_str());
::DebugLog("Source insomnia executable:");
::DebugLog(sourceInsomniaExe.c_str());
#endif
// create the insomnia-$VERSION.exe file
std::string tmpExe(currentPath);
tmpExe.append("\\insomnia-");
tmpExe.append(INSOMNIA_VERSION);
tmpExe.append(".exe");
#ifdef DEBUG
::DebugLog("Creating insomnia executable:");
::DebugLog(tmpExe.c_str());
::DebugLog("Copying file");
#endif
if (!::CopyFile(sourceInsomniaExe.c_str(), tmpExe.c_str(), FALSE)) {
#ifdef DEBUG
DebugLog("Could not copy file.");
#endif
return ::ExitWithWarning(nCmdShow,
"Cannot read or write to executable folder.");
}
if (!::CreateProcess(NULL, (LPSTR)tmpExe.c_str(), NULL, NULL, TRUE, 0, NULL,
currentPath.c_str(), &si, &pi)) {
#ifdef DEBUG
::DebugLog("Could not create process:");
::DebugLog(lpCmdLine);
::DebugLog(__TIME__);
::CloseHandle(outrd);
::CloseHandle(outwr);
#endif
return ::ExitWithWarning(nCmdShow, "Unable to Launch Insomnia.");
}
// yes, close the write handle here, trust me
::CloseHandle(outwr);
// loops until the pipe is closed because the write handle is closed
while (::ReadFile(outrd, insomniaOutputBuffer,
sizeof(insomniaOutputBuffer) - 1, &insomniaOutputBytesRead,
NULL) &&
insomniaOutputBytesRead > 0) {
::WriteFile(::GetStdHandle(STD_OUTPUT_HANDLE), insomniaOutputBuffer,
insomniaOutputBytesRead, NULL, NULL);
}
// no more to read
::CloseHandle(outrd);
// wait for the process to finish (probably arlready done since the read
// handle is not readable)
::WaitForSingleObject(pi.hProcess, INFINITE);
// release the handles
::CloseHandle(pi.hProcess);
::CloseHandle(pi.hThread);
// finally, delete the insomnia-$VERSION.exe file after waiting up to 3s for
// the handle to fully release
for (int i = 0; i < 2; i++) {
Sleep(1000);
if (!::DeleteFile(tmpExe.c_str())) {
#ifdef DEBUG
DWORD lastErr = ::GetLastError();
::DebugLog("Attempted to delete file:");
::DebugLog(tmpExe.c_str());
::DebugLog("Return value:");
::DebugLog(std::to_string(lastErr).c_str());
#endif
} else {
break;
}
}
#ifdef DEBUG
::CloseHandle(hDebugLog);
#endif
return 0;
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<disableWindowFiltering xmlns="http://schemas.microsoft.com/SMI/2011/WindowsSettings">true</disableWindowFiltering>
</asmv3:windowsSettings>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true/pm</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- windows R2 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<maxversiontested Id="10.0.18362.0"/>
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,3 @@
#define IDR_INSOMNIA 103
#include <windows.h>

View File

@@ -0,0 +1,31 @@
#include "resource.h"
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
1 ICON "insomnia.ico"
1 VERSIONINFO
FILEVERSION __MAJOR__,__MINOR__,__PATCH__,0
PRODUCTVERSION __MAJOR__,__MINOR__,__PATCH__,0
FILEOS 0x40004
FILETYPE 0x1
{
BLOCK "StringFileInfo"
{
BLOCK "040904b0"
{
VALUE "CompanyName", "Kong"
VALUE "FileDescription", "Insomnia"
VALUE "FileVersion", "__MAJOR__.__MINOR__.__PATCH____TAG__"
VALUE "InternalName", "Insomnia"
VALUE "LegalCopyright", "Copyright \xA9 __YEAR__ Kong"
VALUE "OriginalFilename", ""
VALUE "ProductName", "Insomnia"
VALUE "ProductVersion", "__MAJOR__.__MINOR__.__PATCH__.0"
VALUE "SquirrelAwareVersion", "1"
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0409, 0x04B0
}
}
1 MANIFEST "manifest.txt"

View File

@@ -35,6 +35,8 @@ import type { ToastNotification } from './ui/components/toast';
const dataPath = process.env.INSOMNIA_DATA_PATH || path.join(app.getPath('userData'), '../', isDevelopment() ? 'insomnia-app' : userDataFolder);
app.setPath('userData', dataPath);
initializeLogging();
initializeSentry();
registerInsomniaProtocols();
@@ -44,7 +46,6 @@ if (checkIfRestartNeeded()) {
process.exit(0);
}
initializeLogging();
log.info(`Running version ${getAppVersion()}`);
// So if (window) checks don't throw

View File

@@ -2,6 +2,8 @@ import { spawn } from 'child_process';
import { app } from 'electron';
import path from 'path';
import log from '../common/log';
function run(args: readonly string[] | undefined, done: (...args: any[]) => void) {
const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe');
spawn(updateExe, args, {
@@ -15,7 +17,12 @@ export function checkIfRestartNeeded() {
}
const cmd = process.argv[1];
console.log('[main] processing squirrel command `%s`', cmd);
if (!cmd) {
return false;
}
log.info('[main] processing squirrel command `%s`', cmd);
const target = path.basename(process.execPath);
switch (cmd) {