diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 9d12403b9b..ba9543b163 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -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 diff --git a/.github/workflows/release-recurring.yml b/.github/workflows/release-recurring.yml index f2f79a3c2d..b3fd618bd9 100644 --- a/.github/workflows/release-recurring.yml +++ b/.github/workflows/release-recurring.yml @@ -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 diff --git a/.gitignore b/.gitignore index f791bf7bca..65dcfc33f0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ dist .history *.node rootCA2.* +*.exe +*.o +final.cpp +insomnia.ico +final.rc diff --git a/.vscode/settings.json b/.vscode/settings.json index 47b9efe612..95e8aaa731 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,4 +41,7 @@ "[typescript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, + "[cpp]": { + "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" + }, } diff --git a/build-secure-wrapper.sh b/build-secure-wrapper.sh new file mode 100644 index 0000000000..91dc687664 --- /dev/null +++ b/build-secure-wrapper.sh @@ -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." diff --git a/packages/insomnia/src/cpp/insomnia.cpp b/packages/insomnia/src/cpp/insomnia.cpp new file mode 100644 index 0000000000..ee56bd86b3 --- /dev/null +++ b/packages/insomnia/src/cpp/insomnia.cpp @@ -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 +#include +#include +#include +#include + +// #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; +} diff --git a/packages/insomnia/src/cpp/manifest.txt b/packages/insomnia/src/cpp/manifest.txt new file mode 100644 index 0000000000..e5710641b8 --- /dev/null +++ b/packages/insomnia/src/cpp/manifest.txt @@ -0,0 +1,34 @@ + + + + + true + + + true/pm + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/insomnia/src/cpp/resource.h b/packages/insomnia/src/cpp/resource.h new file mode 100644 index 0000000000..b81bf95cf7 --- /dev/null +++ b/packages/insomnia/src/cpp/resource.h @@ -0,0 +1,3 @@ +#define IDR_INSOMNIA 103 + +#include diff --git a/packages/insomnia/src/cpp/resources.rc b/packages/insomnia/src/cpp/resources.rc new file mode 100644 index 0000000000..ccf7830650 --- /dev/null +++ b/packages/insomnia/src/cpp/resources.rc @@ -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" diff --git a/packages/insomnia/src/main.development.ts b/packages/insomnia/src/main.development.ts index ec077e5ebc..73fe7ffc66 100644 --- a/packages/insomnia/src/main.development.ts +++ b/packages/insomnia/src/main.development.ts @@ -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 diff --git a/packages/insomnia/src/main/squirrel-startup.ts b/packages/insomnia/src/main/squirrel-startup.ts index 16c8f4bd0f..72d92972ec 100644 --- a/packages/insomnia/src/main/squirrel-startup.ts +++ b/packages/insomnia/src/main/squirrel-startup.ts @@ -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) {