From d3b2ba66739758c6bffec4bd8dc21f8c36494927 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:02:10 +0000 Subject: [PATCH] Fix JSON injection vulnerability in custom payload handling - Replaced manual string escaping with `JSON.stringify` to correctly handle all special characters. - Handled quoting logic for placeholders inside and outside quotes. - Escaped `$` characters in replacement values to prevent regex replacement issues. - Added regression tests in `tests/security.test.js`. Co-authored-by: cmuench <211294+cmuench@users.noreply.github.com> --- tests/security.test.js | 97 ++++++++++++++++++++++++++++++++++++++++++ utils/utils.js | 16 +++++-- 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 tests/security.test.js diff --git a/tests/security.test.js b/tests/security.test.js new file mode 100644 index 0000000..7f50d2d --- /dev/null +++ b/tests/security.test.js @@ -0,0 +1,97 @@ +const { sendWebhook } = require('../utils/utils'); + +// Mock browser API +const mockBrowser = { + tabs: { + query: jest.fn() + }, + runtime: { + getBrowserInfo: jest.fn(), + getPlatformInfo: jest.fn() + }, + i18n: { + getMessage: jest.fn((key) => key) + } +}; + +global.browser = mockBrowser; +global.fetch = jest.fn(); +global.console.error = jest.fn(); + +describe('sendWebhook Vulnerability and bugs', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockBrowser.tabs.query.mockResolvedValue([{ + title: 'Test Page', + url: 'http://example.com', + id: 1, + windowId: 1, + index: 0, + pinned: false, + audible: false, + incognito: false, + status: 'complete' + }]); + mockBrowser.runtime.getBrowserInfo.mockResolvedValue({ name: 'Firefox' }); + mockBrowser.runtime.getPlatformInfo.mockResolvedValue({ os: 'linux' }); + global.fetch.mockResolvedValue({ ok: true }); + }); + + test('should correctly handle backslash in payload (fix JSON syntax error)', async () => { + const webhook = { + url: 'http://localhost/hook', + customPayload: '{ "title": "{{tab.title}}" }', + method: 'POST' + }; + + mockBrowser.tabs.query.mockResolvedValue([{ + title: '\\', + url: 'http://example.com' + }]); + + await sendWebhook(webhook, false); + + const callArgs = global.fetch.mock.calls[0]; + const body = JSON.parse(callArgs[1].body); + expect(body.title).toBe('\\'); + }); + + test('should correctly handle escaped quote in payload', async () => { + const webhook = { + url: 'http://localhost/hook', + customPayload: '{ "title": "{{tab.title}}" }', + method: 'POST' + }; + + mockBrowser.tabs.query.mockResolvedValue([{ + title: '\\"', + url: 'http://example.com' + }]); + + await sendWebhook(webhook, false); + + const callArgs = global.fetch.mock.calls[0]; + const body = JSON.parse(callArgs[1].body); + expect(body.title).toBe('\\"'); + }); + + test('should correctly handle dollar signs in payload', async () => { + const webhook = { + url: 'http://localhost/hook', + customPayload: '{ "title": "{{tab.title}}" }', + method: 'POST' + }; + + // '$&' is a special replacement pattern (inserts matched string) + mockBrowser.tabs.query.mockResolvedValue([{ + title: '$&', + url: 'http://example.com' + }]); + + await sendWebhook(webhook, false); + + const callArgs = global.fetch.mock.calls[0]; + const body = JSON.parse(callArgs[1].body); + expect(body.title).toBe('$&'); + }); +}); diff --git a/utils/utils.js b/utils/utils.js index 98dd95d..f432f3f 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -158,9 +158,19 @@ async function sendWebhook(webhook, isTest = false) { Object.entries(replacements).forEach(([placeholder, value]) => { const isPlaceholderInQuotes = customPayloadStr.match(new RegExp(`"[^"]*${placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^"]*"`)); - const replaceValue = typeof value === 'string' - ? (isPlaceholderInQuotes ? value.replace(/"/g, '\\"') : `"${value.replace(/"/g, '\\"')}"`) - : (value === undefined ? 'null' : JSON.stringify(value)); + let replaceValue; + if (typeof value === 'string') { + replaceValue = JSON.stringify(value); + if (isPlaceholderInQuotes) { + // Remove the surrounding quotes added by JSON.stringify + replaceValue = replaceValue.slice(1, -1); + } + } else { + replaceValue = value === undefined ? 'null' : JSON.stringify(value); + } + + // Escape special replacement patterns ($) to prevent them from being interpreted by String.prototype.replace + replaceValue = replaceValue.replace(/\$/g, '$$$$'); customPayloadStr = customPayloadStr.replace( new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),