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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to GitHub Devwatch will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Security
- GitHub sign-in sessions now stay in Chrome session storage only instead of persisting to local extension storage
- Added validation for the GitHub device-flow verification URL before opening a browser tab
- Tightened remote image handling for activity avatars and extension page CSP rules

## [1.0.2] - 2025-11-19

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ GitHub Devwatch collects and stores the following data **locally on your device

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

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

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

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

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ These are better suited for regular issues:
The extension includes several concrete protections, but this project has not been through a formal external security audit.

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

### Content Security Policy
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@
"128": "icons/icon128.png"
},
"content_security_policy": {
"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'"
"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'"
}
}
2 changes: 1 addition & 1 deletion options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ <h3>Connect GitHub</h3>
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
</svg>
<span><strong>Security:</strong> Your GitHub sign-in session is encrypted with AES-GCM and stored locally on your device.</span>
<span><strong>Security:</strong> Your GitHub sign-in stays in Chrome session storage and is cleared when the browser session ends.</span>
</p>
</div>
<div class="info-box info-box-compact">
Expand Down
3 changes: 2 additions & 1 deletion options/options.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { applyTheme, formatDateVerbose } from '../shared/utils.js';
import { getAuthSession, getAccessToken, getLocalItems, getWatchedRepos, setLocalItem, setWatchedRepos } from '../shared/storage-helpers.js';
import { clearAuthSession, getAuthSession, getAccessToken, getLocalItems, getWatchedRepos, setLocalItem, setWatchedRepos } from '../shared/storage-helpers.js';
import { createHeaders } from '../shared/github-api.js';
import { STORAGE_CONFIG, VALIDATION_PATTERNS } from '../shared/config.js';
import { validateRepository } from '../shared/repository-validator.js';
Expand Down Expand Up @@ -1052,6 +1052,7 @@ async function resetSettings() {

try {
// Clear all storage (both sync and local)
await clearAuthSession();
await chrome.storage.sync.clear();
await chrome.storage.local.clear();

Expand Down
2 changes: 1 addition & 1 deletion popup/views/onboarding-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ async function renderTokenStep() {
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
</svg>
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.
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.
</p>
`;
}
Expand Down
10 changes: 10 additions & 0 deletions shared/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { API_CONFIG, OAUTH_CONFIG } from './config.js';
import { isValidGitHubAuthUrl } from './security.js';

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

if (!isValidGitHubAuthUrl(targetUrl)) {
throw createOAuthError(
'GitHub sign-in returned an unexpected verification URL.',
'invalid_verification_url'
);
}

if (chrome?.tabs?.create) {
chrome.tabs.create({ url: targetUrl });
}

return targetUrl;
}

export async function pollForGitHubAccessToken(deviceCodeData, options = {}) {
Expand Down
106 changes: 0 additions & 106 deletions shared/crypto-utils.js

This file was deleted.

6 changes: 4 additions & 2 deletions shared/dom-optimizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { UI_CONFIG } from './config.js';
import { SNOOZE_ICON, CHECK_ICON } from './icons.js';
import { sanitizeImageUrl } from './sanitize.js';

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

Expand Down Expand Up @@ -576,4 +578,4 @@ function escapeHtml(text) {
}

// Export classes and utilities
export { DOMOptimizer, ActivityListRenderer, escapeHtml };
export { DOMOptimizer, ActivityListRenderer, escapeHtml };
22 changes: 22 additions & 0 deletions shared/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ export function isValidGitHubUrl(url) {
}
}

/**
* Validates the GitHub device flow verification page before opening it.
* Restricts auth navigation to the expected GitHub login path.
* @param {string} url - URL to validate
* @returns {boolean} - True if URL is safe for the OAuth device flow
*/
export function isValidGitHubAuthUrl(url) {
if (!url || typeof url !== 'string') {
return false;
}

try {
const parsed = new URL(url);

return parsed.protocol === 'https:'
&& parsed.hostname === 'github.com'
&& parsed.pathname === '/login/device';
} catch {
return false;
}
}

/**
* Safely opens a URL in a new tab only if it's a valid GitHub URL
* @param {string} url - URL to open
Expand Down
Loading
Loading