Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions tests/security.test.js
Original file line number Diff line number Diff line change
@@ -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('$&');
});
});
16 changes: 13 additions & 3 deletions utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down