Skip to content

Commit a97ad0b

Browse files
committed
Copilot security audit and enhancements
1 parent 3ca15fc commit a97ad0b

File tree

8 files changed

+5393
-11
lines changed

8 files changed

+5393
-11
lines changed

package-lock.json

Lines changed: 5184 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/background.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { todayLocalDate } from './utils/date';
99
import { checkPomodoro } from './functions/pomodoro';
1010
import { Messages } from './utils/messages';
1111
import { injectTabsRepositorySingleton } from './repository/inject-tabs-repository';
12+
import { isValidMessage, sanitizeBackupData } from './utils/security';
1213

1314
logger.log('Start background script');
1415
let pomodoroTimer: number;
@@ -77,6 +78,12 @@ scheduleJobs();
7778
initTracker();
7879

7980
Browser.runtime.onMessage.addListener(async message => {
81+
// SECURITY FIX: Validate message structure to prevent malicious messages
82+
if (!isValidMessage(message)) {
83+
console.warn('Invalid message received:', message);
84+
return;
85+
}
86+
8087
if (message == Messages.ClearAllData) {
8188
const storage = injectStorage();
8289
const repo = await injectTabsRepositorySingleton();
@@ -85,7 +92,9 @@ Browser.runtime.onMessage.addListener(async message => {
8592
}
8693
if (message.message == Messages.Restore) {
8794
const storage = injectStorage();
88-
await storage.saveTabs(message.data);
95+
// SECURITY FIX: Sanitize backup data before processing
96+
const sanitizedData = sanitizeBackupData(message.data);
97+
await storage.saveTabs(sanitizedData);
8998
const repo = await injectTabsRepositorySingleton();
9099
repo.initAsync();
91100
}

src/components/WhiteList.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { injectStorage } from '../storage/inject-storage';
4545
import { StorageParams } from '../storage/storage-params';
4646
import { isDomainEquals } from '../utils/common';
4747
import { extractHostname } from '../utils/extract-hostname';
48+
import { sanitizeUserInput } from '../utils/security';
4849
4950
const { t } = useI18n();
5051
@@ -60,16 +61,19 @@ onMounted(async () => {
6061
});
6162
6263
function addToWhiteList() {
64+
// SECURITY FIX: Sanitize user input before processing
65+
const sanitizedInput = sanitizeUserInput(newWebsiteForWhiteList.value || '');
66+
6367
const existingItem = whiteList.value?.find(x =>
64-
isDomainEquals(extractHostname(x), extractHostname(newWebsiteForWhiteList.value!)),
68+
isDomainEquals(extractHostname(x), extractHostname(sanitizedInput)),
6569
);
6670
if (existingItem !== undefined) {
6771
notification.notify({
6872
title: 'You have already added this site',
6973
type: 'error',
7074
});
7175
} else {
72-
const newWebsite = extractHostname(newWebsiteForWhiteList.value!);
76+
const newWebsite = extractHostname(sanitizedInput);
7377
whiteList.value?.push(newWebsite);
7478
save(whiteList.value);
7579
newWebsiteForWhiteList.value = '';

src/functions/useRestoreData.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
import Browser from 'webextension-polyfill';
22
import { Messages } from '../utils/messages';
3+
import { isValidBackupData, sanitizeBackupData } from '../utils/security';
34

45
export async function useRestoreData(json: string) {
5-
if (json != null && json != undefined && json != '') {
6+
if (!json || typeof json !== 'string') {
7+
throw new Error('Invalid backup data: empty or invalid input');
8+
}
9+
10+
try {
11+
// SECURITY FIX: Validate JSON structure before parsing to prevent code injection
612
const data = JSON.parse(json);
13+
14+
// Validate the data structure
15+
if (!isValidBackupData(data)) {
16+
throw new Error('Invalid backup data structure');
17+
}
18+
19+
// Sanitize the data to ensure safety
20+
const sanitizedData = sanitizeBackupData(data);
21+
722
await Browser.runtime.sendMessage({
823
message: Messages.Restore,
9-
data: data,
24+
data: sanitizedData,
1025
});
26+
} catch (error) {
27+
console.error('Invalid backup file:', error);
28+
throw new Error('Invalid or corrupted backup file');
1129
}
1230
}

src/manifest.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@
3030
"default_popup": "src/popup.html",
3131
"default_title": "Web Activity Time Tracker"
3232
},
33-
"web_accessible_resources": [{
34-
"resources": ["assets/pomodoro-sounds/*.mp3"],
35-
"matches": ["<all_urls>"]
36-
}]
33+
"web_accessible_resources": [
34+
{
35+
"resources": ["assets/pomodoro-sounds/*.mp3"],
36+
"matches": ["<all_urls>"]
37+
}
38+
],
39+
"content_security_policy": {
40+
"extension_pages": "script-src 'self'; object-src 'none'; base-uri 'none';"
41+
}
3742
}

src/pages/Block.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import { BLOCK_DEFERRAL_DEFAULT, StorageParams } from '../storage/storage-params
4343
import { convertLimitTimeToString } from '../utils/converter';
4444
import PromoClearYouTube from '../components/PromoClearYouTube.vue';
4545
import { canDefering, defering } from '../functions/deferList';
46+
import { isValidRedirectUrl } from '../utils/security';
47+
import Browser from 'webextension-polyfill';
4648
4749
const { t } = useI18n();
4850
@@ -81,7 +83,14 @@ async function deferring() {
8183
haveToShowDeffering.value
8284
) {
8385
await defering(webSite.value, 5);
84-
if (sourceUrl.value != '') window.location.replace(sourceUrl.value);
86+
87+
// SECURITY FIX: Validate URL before redirect to prevent open redirect attacks
88+
if (sourceUrl.value && isValidRedirectUrl(sourceUrl.value)) {
89+
window.location.replace(sourceUrl.value);
90+
} else {
91+
// Redirect to safe default page if URL is invalid or empty
92+
window.location.replace(Browser.runtime.getURL('src/dashboard.html'));
93+
}
8594
}
8695
}
8796
</script>

src/utils/block-page.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { sanitizeFaviconUrl } from './security';
2+
13
export enum BlockParams {
24
Domain = 'domain',
35
URL = 'url',
@@ -20,7 +22,11 @@ export function getValueFromQuery(url: string) {
2022
const urlObj = new URL(url);
2123
const domain = urlObj.searchParams.get(BlockParams.Domain);
2224
const sourceUrl = urlObj.searchParams.get(BlockParams.URL);
23-
const favicon = urlObj.searchParams.get(BlockParams.Favicon);
25+
let favicon = urlObj.searchParams.get(BlockParams.Favicon);
26+
27+
// SECURITY FIX: Sanitize favicon URL to prevent XSS/SSRF attacks
28+
favicon = sanitizeFaviconUrl(favicon);
29+
2430
const limitTime = Number(urlObj.searchParams.get(BlockParams.LimitTime));
2531
const summaryCounter = Number(urlObj.searchParams.get(BlockParams.SummaryCounter));
2632

src/utils/security.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { NO_FAVICON } from './consts';
2+
3+
/**
4+
* Security utilities for input validation and sanitization
5+
*/
6+
7+
/**
8+
* Validates if a URL is safe for redirection
9+
* Only allows http/https protocols and optionally validates against whitelist
10+
*/
11+
export function isValidRedirectUrl(url: string): boolean {
12+
if (!url || typeof url !== 'string') {
13+
return false;
14+
}
15+
16+
try {
17+
const urlObj = new URL(url);
18+
19+
// Only allow http/https protocols to prevent javascript:, data:, etc.
20+
if (!['http:', 'https:'].includes(urlObj.protocol)) {
21+
return false;
22+
}
23+
24+
// Check for dangerous schemes in the URL string (case-insensitive)
25+
const lowerUrl = url.toLowerCase();
26+
if (
27+
lowerUrl.includes('javascript:') ||
28+
lowerUrl.includes('data:') ||
29+
lowerUrl.includes('vbscript:') ||
30+
lowerUrl.includes('file:')
31+
) {
32+
return false;
33+
}
34+
35+
return true;
36+
} catch {
37+
return false;
38+
}
39+
}
40+
41+
/**
42+
* Sanitizes and validates favicon URLs
43+
* Returns NO_FAVICON constant for invalid URLs
44+
*/
45+
export function sanitizeFaviconUrl(favicon: string | null): string {
46+
if (!favicon || typeof favicon !== 'string') {
47+
return NO_FAVICON;
48+
}
49+
50+
try {
51+
const url = new URL(favicon);
52+
53+
// Only allow http/https protocols
54+
if (!['http:', 'https:'].includes(url.protocol)) {
55+
return NO_FAVICON;
56+
}
57+
58+
// Check for dangerous schemes
59+
const lowerUrl = favicon.toLowerCase();
60+
if (
61+
lowerUrl.includes('javascript:') ||
62+
lowerUrl.includes('data:') ||
63+
lowerUrl.includes('vbscript:')
64+
) {
65+
return NO_FAVICON;
66+
}
67+
68+
return favicon;
69+
} catch {
70+
// Invalid URL
71+
return NO_FAVICON;
72+
}
73+
}
74+
75+
/**
76+
* Sanitizes user input to prevent XSS and other injection attacks
77+
*/
78+
export function sanitizeUserInput(input: string): string {
79+
if (typeof input !== 'string') {
80+
return '';
81+
}
82+
83+
// Remove dangerous characters and scripts
84+
return input
85+
.replace(/<script[^>]*>.*?<\/script>/gi, '') // Remove script tags
86+
.replace(/javascript:/gi, '') // Remove javascript: protocol
87+
.replace(/data:/gi, '') // Remove data: protocol
88+
.replace(/vbscript:/gi, '') // Remove vbscript: protocol
89+
.replace(/on\w+\s*=/gi, '') // Remove event handlers like onclick=
90+
.replace(/<[^>]*>/g, '') // Remove all HTML tags
91+
.trim()
92+
.substring(0, 1000); // Limit length to prevent DoS
93+
}
94+
95+
/**
96+
* Validates backup data structure to prevent code injection
97+
*/
98+
export function isValidBackupData(data: any): boolean {
99+
if (!Array.isArray(data)) {
100+
return false;
101+
}
102+
103+
return data.every(item => {
104+
return (
105+
typeof item === 'object' &&
106+
item !== null &&
107+
typeof item.url === 'string' &&
108+
typeof item.summaryTime === 'number' &&
109+
item.summaryTime >= 0
110+
);
111+
});
112+
}
113+
114+
/**
115+
* Sanitizes backup data to ensure safety
116+
*/
117+
export function sanitizeBackupData(data: any[]): any[] {
118+
return data.map(item => ({
119+
url: sanitizeUserInput(item.url || ''),
120+
summaryTime: Math.max(0, Math.floor(Number(item.summaryTime) || 0)),
121+
counter: Math.max(0, Math.floor(Number(item.counter) || 0)),
122+
favicon: sanitizeFaviconUrl(item.favicon),
123+
days: Array.isArray(item.days) ? item.days.map(sanitizeDayData) : [],
124+
}));
125+
}
126+
127+
/**
128+
* Sanitizes day data within backup
129+
*/
130+
function sanitizeDayData(day: any): any {
131+
return {
132+
date: sanitizeUserInput(day.date || ''),
133+
summary: Math.max(0, Math.floor(Number(day.summary) || 0)),
134+
counter: Math.max(0, Math.floor(Number(day.counter) || 0)),
135+
};
136+
}
137+
138+
/**
139+
* Validates that a message has the expected structure
140+
*/
141+
export function isValidMessage(message: any): boolean {
142+
return (
143+
message &&
144+
(typeof message === 'string' ||
145+
(typeof message === 'object' && (message.type || message.message)))
146+
);
147+
}

0 commit comments

Comments
 (0)