Skip to content

Commit edec36e

Browse files
authored
Harden auth storage and sign-in guards (#23)
Keep GitHub sign-in data in session storage instead of persisting it to local extension storage, and clear legacy auth data on access. Add a stricter device-flow URL check before opening GitHub tabs, restore avatar URL sanitization in the optimized renderer, and tighten the extension CSP to match the allowed image sources. Update tests and user-facing security copy to reflect the new session- only auth behavior. Co-authored-by: jonmartin721 <jonmartin721@users.noreply.github.com>
1 parent 163c29c commit edec36e

18 files changed

Lines changed: 171 additions & 215 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to GitHub Devwatch will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Security
11+
- GitHub sign-in sessions now stay in Chrome session storage only instead of persisting to local extension storage
12+
- Added validation for the GitHub device-flow verification URL before opening a browser tab
13+
- Tightened remote image handling for activity avatars and extension page CSP rules
14+
815
## [1.0.2] - 2025-11-19
916

1017
### Fixed

PRIVACY.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ GitHub Devwatch collects and stores the following data **locally on your device
1414

1515
1. **GitHub OAuth Session**
1616
- Created when you connect GitHub through the built-in device-flow sign-in
17-
- Stored by the extension in Chrome storage
18-
- Current builds encrypt the auth session before writing it to local storage and keep a decrypted session copy while the extension is running
17+
- Stored by the extension in Chrome session storage for the current browser session only
18+
- Cleared when the browser session ends or when you disconnect GitHub in DevWatch
1919
- Used only to authenticate with GitHub's API
2020
- Not sent to third-party services operated by this project
2121
- Never shared with anyone
@@ -56,7 +56,7 @@ All data collected is used exclusively to provide the extension's functionality:
5656

5757
- The extension uses Chrome storage APIs for settings, cached activity, and GitHub sign-in handling
5858
- Settings and repository lists can optionally sync across your Chrome browsers if you use Chrome Sync
59-
- GitHub sign-in data uses local and session storage rather than Chrome sync
59+
- GitHub sign-in data uses session storage rather than Chrome sync and is not persisted across browser restarts
6060
- You can clear all data at any time by uninstalling the extension or using Chrome's "Clear extension data" feature
6161

6262
## Third-Party Services
@@ -112,7 +112,7 @@ You have complete control over your data:
112112
Current builds include several concrete safeguards:
113113

114114
- All API requests use HTTPS
115-
- The GitHub auth session is encrypted before it is persisted locally
115+
- The GitHub auth session is kept in session storage for the current browser session only
116116
- The codebase includes input sanitization and GitHub URL validation checks
117117
- Extension pages use a Content Security Policy
118118

SECURITY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ These are better suited for regular issues:
3333
The extension includes several concrete protections, but this project has not been through a formal external security audit.
3434

3535
### GitHub Sign-In Storage
36-
- GitHub auth sessions are encrypted before they are written to local extension storage
37-
- A decrypted copy may be cached in session storage while the extension is running
36+
- GitHub auth sessions are kept in `chrome.storage.session` for the current browser session only
37+
- Legacy on-disk auth storage is cleared by current builds during sign-in handling
3838
- Never transmitted to third-party servers
3939

4040
### Content Security Policy

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
"128": "icons/icon128.png"
3535
},
3636
"content_security_policy": {
37-
"extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://api.github.com https://github.com https://registry.npmjs.org; img-src 'self' https: data:; default-src 'self'; style-src 'self'"
37+
"extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://api.github.com https://github.com https://registry.npmjs.org; img-src 'self' data: https://github.com https://*.github.com https://githubusercontent.com https://*.githubusercontent.com; default-src 'self'; style-src 'self'"
3838
}
3939
}

options/options.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ <h3>Connect GitHub</h3>
9494
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
9595
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
9696
</svg>
97-
<span><strong>Security:</strong> Your GitHub sign-in session is encrypted with AES-GCM and stored locally on your device.</span>
97+
<span><strong>Security:</strong> Your GitHub sign-in stays in Chrome session storage and is cleared when the browser session ends.</span>
9898
</p>
9999
</div>
100100
<div class="info-box info-box-compact">

options/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { applyTheme, formatDateVerbose } from '../shared/utils.js';
2-
import { getAuthSession, getAccessToken, getLocalItems, getWatchedRepos, setLocalItem, setWatchedRepos } from '../shared/storage-helpers.js';
2+
import { clearAuthSession, getAuthSession, getAccessToken, getLocalItems, getWatchedRepos, setLocalItem, setWatchedRepos } from '../shared/storage-helpers.js';
33
import { createHeaders } from '../shared/github-api.js';
44
import { STORAGE_CONFIG, VALIDATION_PATTERNS } from '../shared/config.js';
55
import { validateRepository } from '../shared/repository-validator.js';
@@ -1052,6 +1052,7 @@ async function resetSettings() {
10521052

10531053
try {
10541054
// Clear all storage (both sync and local)
1055+
await clearAuthSession();
10551056
await chrome.storage.sync.clear();
10561057
await chrome.storage.local.clear();
10571058

popup/views/onboarding-view.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ async function renderTokenStep() {
379379
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
380380
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
381381
</svg>
382-
Your GitHub sign-in session is encrypted with AES-GCM encryption and stored securely on your device. It's only used for GitHub API access and never shared.
382+
Your GitHub sign-in stays in Chrome session storage for the current browser session only. It's used only for GitHub API access and is cleared when the browser session ends.
383383
</p>
384384
`;
385385
}

shared/auth.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { API_CONFIG, OAUTH_CONFIG } from './config.js';
2+
import { isValidGitHubAuthUrl } from './security.js';
23

34
function getStorageValue(area, key) {
45
return new Promise((resolve) => {
@@ -181,9 +182,18 @@ export async function requestGitHubDeviceCode() {
181182
export function openGitHubDevicePage(deviceCodeData) {
182183
const targetUrl = deviceCodeData.verificationUriComplete || deviceCodeData.verificationUri || OAUTH_CONFIG.DEVICE_VERIFY_URL;
183184

185+
if (!isValidGitHubAuthUrl(targetUrl)) {
186+
throw createOAuthError(
187+
'GitHub sign-in returned an unexpected verification URL.',
188+
'invalid_verification_url'
189+
);
190+
}
191+
184192
if (chrome?.tabs?.create) {
185193
chrome.tabs.create({ url: targetUrl });
186194
}
195+
196+
return targetUrl;
187197
}
188198

189199
export async function pollForGitHubAccessToken(deviceCodeData, options = {}) {

shared/crypto-utils.js

Lines changed: 0 additions & 106 deletions
This file was deleted.

shared/dom-optimizer.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { UI_CONFIG } from './config.js';
77
import { SNOOZE_ICON, CHECK_ICON } from './icons.js';
8+
import { sanitizeImageUrl } from './sanitize.js';
89

910
/**
1011
* Basic DOM renderer with simple caching to avoid unnecessary re-renders
@@ -440,7 +441,8 @@ class ActivityListRenderer {
440441
const sanitizedType = escapeHtml(activity.type);
441442
const sanitizedTypeLabel = escapeHtml(this.getActivityTypeLabel(activity.type));
442443
const sanitizedDescription = activity.description ? escapeHtml(activity.description) : '';
443-
const sanitizedAvatar = activity.authorAvatar || 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"%3E%3C/svg%3E';
444+
const sanitizedAvatar = sanitizeImageUrl(activity.authorAvatar)
445+
|| 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"%3E%3C/svg%3E';
444446
const sanitizedUrl = escapeHtml(activity.url);
445447
const sanitizedId = escapeHtml(activity.id);
446448

@@ -576,4 +578,4 @@ function escapeHtml(text) {
576578
}
577579

578580
// Export classes and utilities
579-
export { DOMOptimizer, ActivityListRenderer, escapeHtml };
581+
export { DOMOptimizer, ActivityListRenderer, escapeHtml };

0 commit comments

Comments
 (0)