Skip to content
Open
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
4 changes: 4 additions & 0 deletions .Jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
**Vulnerability:** The extension's Content Security Policy (CSP) in `manifest.json` lacked explicit `default-src 'none'` and broad network constraints, relying solely on script and object restrictions.
**Learning:** A permissive CSP allows unexpected resource loading and potential data exfiltration if an XSS vulnerability occurs. A strict whitelist (`default-src 'none'`) provides a robust defense-in-depth layer.
**Prevention:** Always define a strict CSP for extensions, setting `default-src 'none'` and explicitly allowing only required origins (e.g., `connect-src`).
## 2026-08-27 - Prevent token leakage via open redirects
**Vulnerability:** The `jFetch` wrapper did not constrain HTTP redirects when sending requests with sensitive credentials (API tokens), making it susceptible to accidental token leakage via cross-origin open redirects.
**Learning:** When implementing network wrappers (e.g., `fetch`) that transmit sensitive credentials like API tokens, conditionally setting `redirect: 'error'` or `redirect: 'manual'` in the fetch options when credentials are provided prevents accidental token leakage via cross-origin open redirects without breaking legitimate API endpoints that rely on standard HTTP redirects.
**Prevention:** Always restrict redirect behavior when sending authorization headers in custom fetch wrappers.
1 change: 1 addition & 0 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async function jFetch(url, options = {}) {
if (typeof token !== 'string') throw new Error('Token must be a string')
if (/[\r\n]/.test(token)) throw new Error('Invalid token: contains newline')
headers.Authorization = `token ${token}`
rest.redirect = rest.redirect || 'error'
}

const controller = new AbortController()
Expand Down
22 changes: 22 additions & 0 deletions tests/background.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,28 @@ describe('jFetch', () => {
assert.strictEqual(capturedHeaders.Authorization, 'token valid-token')
})

it('should set redirect to error when token is provided and redirect is undefined', async () => {
const { sandbox } = setupEnvironment()
let capturedOptions = null
sandbox.fetch = async (_url, options) => {
capturedOptions = options
return { ok: true }
}
await sandbox.test_jFetch('https://api.github.com/test', { token: 'secret-token' })
assert.strictEqual(capturedOptions.redirect, 'error')
})

it('should preserve existing redirect option when token is provided', async () => {
const { sandbox } = setupEnvironment()
let capturedOptions = null
sandbox.fetch = async (_url, options) => {
capturedOptions = options
return { ok: true }
}
await sandbox.test_jFetch('https://api.github.com/test', { token: 'secret-token', redirect: 'manual' })
assert.strictEqual(capturedOptions.redirect, 'manual')
})

it('should throw an error for HTTP 500 status code', async () => {
const { sandbox } = setupEnvironment()
sandbox.fetch = async () => ({
Expand Down
17 changes: 16 additions & 1 deletion tests/security.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ function setupEnvironment(initialTabs = {}) {

const sandbox = {
chrome: chromeMock,
fetch: async () => ({ ok: true, json: async () => [], text: async () => ")]}'\n\n4\n[[]]" }),
fetch: async (url, options) => {
chromeMock.lastFetch = { url, options }
return { ok: true, json: async () => [], text: async () => ")]}'\n\n4\n[[]]" }
},
setTimeout,
Date,
Promise,
Expand Down Expand Up @@ -312,6 +315,18 @@ describe('jFetch SSRF Security', () => {
message: /Security Error: Refusing to send GitHub token to non-GitHub origin/
})
})

it('should set redirect to error when token is provided and redirect is undefined', async () => {
const { sandbox } = setupEnvironment()
await sandbox.jFetch('https://api.github.com/test', { token: 'secret-token' })
assert.strictEqual(sandbox.chrome.lastFetch.options.redirect, 'error')
})

it('should preserve existing redirect option when token is provided', async () => {
const { sandbox } = setupEnvironment()
await sandbox.jFetch('https://api.github.com/test', { token: 'secret-token', redirect: 'manual' })
assert.strictEqual(sandbox.chrome.lastFetch.options.redirect, 'manual')
})
})

describe('getTabConfig Path Traversal Security', () => {
Expand Down