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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@
**Vulnerability:** XSS risk via unsanitized `<` characters in `JSON.stringify` output injected into `<script type="application/ld+json">`.
**Learning:** `JSON.stringify()` does not automatically escape `<` as `\u003c`. If dynamic or unsanitized content is serialized into a `<script>` tag via `dangerouslySetInnerHTML`, an attacker can include `</script>` to break out of the context and inject malicious scripts.
**Prevention:** Always replace `<` with `\u003c` when injecting JSON output into script tags, e.g., `JSON.stringify(data).replace(/</g, '\\u003c')`.

## 2025-06-06 - Missing Fetch Timeouts
**Vulnerability:** Resource exhaustion risk due to missing timeouts in external API `fetch` calls.
**Learning:** The native `fetch` API does not have a default timeout. If an external API hangs or takes a long time to respond, it can tie up server resources, potentially leading to a Denial of Service (DoS) condition, especially in Server-Side Rendered (SSR) contexts like Next.js or background synchronization scripts.
**Prevention:** Always use `AbortSignal.timeout(TIMEOUT_MS)` in the `signal` option of `fetch` calls to ensure requests fail securely after a reasonable duration rather than hanging indefinitely.
3 changes: 3 additions & 0 deletions scripts/sync-sources.mjs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

https://github.com/felirami/hypersnaporg/blob/df59b28570e1ffda411e10c8e65a8227d6a5cfab/scripts/sync-sources.mjs#L29

fetchGithubJson aborts every request after FETCH_TIMEOUT_MS (10s), but the optional path only treats HTTP 404/409 as recoverable. When an optional call such as getRepoTree() or getLatestRelease() times out, AbortSignal.timeout throws TimeoutError, which is not caught, so buildRepoSnapshot() rethrows and main() terminates. A single slow GitHub API response now fails the entire sync:sources job instead of just omitting that optional field. Consider catching the abort error for optional calls and returning null.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @scripts/sync-sources.mjs around line 29:

`fetchGithubJson` aborts every request after `FETCH_TIMEOUT_MS` (10s), but the `optional` path only treats HTTP `404`/`409` as recoverable. When an optional call such as `getRepoTree()` or `getLatestRelease()` times out, `AbortSignal.timeout` throws `TimeoutError`, which is not caught, so `buildRepoSnapshot()` rethrows and `main()` terminates. A single slow GitHub API response now fails the entire `sync:sources` job instead of just omitting that optional field. Consider catching the abort error for optional calls and returning `null`.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const SOURCE_REPO_ORDER = ["hypersnap", "hypersnap-docs-web", "snap", "protocol"

const DOCS_LINK_LIMIT = 80;
const README_HEADING_LIMIT = 10;
const FETCH_TIMEOUT_MS = 10000;

const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;

Expand All @@ -32,6 +33,7 @@ async function fetchGithubJson(url, { optional = false } = {}) {
"X-GitHub-Api-Version": "2022-11-28",
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
},
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});

if ((response.status === 404 || response.status === 409) && optional) {
Expand All @@ -53,6 +55,7 @@ async function fetchGithubText(url, { optional = false } = {}) {
"X-GitHub-Api-Version": "2022-11-28",
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
},
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});

if ((response.status === 404 || response.status === 409) && optional) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export async function getNetworkStatus(): Promise<NetworkStatus> {
headers: {
Accept: "application/json",
},
signal: AbortSignal.timeout(NODE_PROBE_TIMEOUT_MS),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve memoization for timed status fetches

On /network, NetworkPage calls getNetworkStatus() before rendering NetworkStatusGrid, and the grid calls getNetworkStatus() again. Next's fetch memoization docs state that passing an AbortController signal opts the request out of server-render memoization (https://nextjs.org/docs/app/api-reference/functions/fetch#memoization), so this fresh timeout signal makes that route issue the same public-node /v1/info fetch twice instead of sharing the first result, adding latency and extra external load. Consider adding the timeout in a cached/shared wrapper rather than on the memoized fetch options.

Useful? React with 👍 / 👎.

});

if (!response.ok) {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/snap-market.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SNAP, correctedSnapFdv } from "@/lib/snap";

export const SNAP_MARKET_REVALIDATE = 30;
export const SNAP_MARKET_TIMEOUT_MS = 10_000;

type DexPeriod = {
buys?: number;
Expand Down Expand Up @@ -79,6 +80,7 @@ export async function getSnapMarketData(): Promise<SnapMarketResponse> {
"user-agent": "hypersnap.org market data checker",
},
next: { revalidate: SNAP_MARKET_REVALIDATE },
signal: AbortSignal.timeout(SNAP_MARKET_TIMEOUT_MS),
});

if (!response.ok) {
Expand Down
Loading