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 @@ -21,3 +21,7 @@
**Vulnerability:** The application was using `.startsWith('/async/')` and `.startsWith('/store/')` to protect sensitive endpoints, allowing an attacker to request `/async-bypass` and completely bypass the authentication checks.
**Learning:** Checking route paths purely by checking if they start with a string containing a trailing slash might ignore the root path (without trailing slash), and leaving off the trailing slash might allow matching unintended sibling paths.
**Prevention:** Always verify paths against exact matches (e.g. `=== '/async'`) OR prefix matches using trailing slashes (`.startsWith('/async/')`). Avoid loose prefix matching (`.startsWith('/async')`).
## 2024-08-06 - Prevent SSRF in MQ Destinations
**Vulnerability:** URLs fetched in background queue handlers (`MQDestiny.ts` and `MQCallback.ts`) were not validated for secure protocols before being passed to `fetch()`, potentially allowing SSRF via unsupported schemes like `file://` or `ftp://`.
**Learning:** Even internal queue processors should distrust inputs hydrated from the database/storage, as they might have bypassed frontend validation or been manually inserted.
**Prevention:** Always parse dynamic URLs using `new URL()` and explicitly enforce `http:` or `https:` protocols before using them in server-side `fetch()` requests.
17 changes: 17 additions & 0 deletions src/mq/MQCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ export default async function(rawmsg: Message<unknown>, env: Env) {
return;
}

try {
const callbackUrl = new URL(asyncContent.callback);
if (callbackUrl.protocol !== 'http:' && callbackUrl.protocol !== 'https:') {
await MQStore(rawmsg, env, {
type: 'error',
resettime: true
});
return;
}
} catch (e) {
await MQStore(rawmsg, env, {
type: 'error',
resettime: true
});
return;
}

try {
let headers = new Headers();
if (asyncContent.headersCallback) {
Expand Down
17 changes: 17 additions & 0 deletions src/mq/MQDestiny.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ export default async function(rawmsg: Message<unknown>, env: Env) {
return;
}

try {
const destinyUrl = new URL(asyncContent.destiny);
if (destinyUrl.protocol !== 'http:' && destinyUrl.protocol !== 'https:') {
await MQStore(rawmsg, env, {
type: 'error',
resettime: true
});
return;
}
} catch (e) {
await MQStore(rawmsg, env, {
type: 'error',
resettime: true
});
return;
}

try {
let headers = new Headers();
if (asyncContent.headersDestiny) {
Expand Down
Loading