Skip to content

Sec/hardening#77

Merged
dskvr merged 3 commits intomainfrom
sec/hardening
Mar 11, 2026
Merged

Sec/hardening#77
dskvr merged 3 commits intomainfrom
sec/hardening

Conversation

@dskvr
Copy link

@dskvr dskvr commented Mar 11, 2026

No description provided.

Copilot AI review requested due to automatic review settings March 11, 2026 11:29
@dskvr dskvr requested review from hzrd149 and removed request for Copilot March 11, 2026 11:30
Copilot AI review requested due to automatic review settings March 11, 2026 13:24
@dskvr dskvr merged commit 958ea11 into main Mar 11, 2026
7 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR focuses on hardening the local gateway/server behavior to reduce common web security risks (XSS, overly-permissive networking defaults) when serving nsites.

Changes:

  • Adds HTML escaping and applies additional response security headers throughout the gateway.
  • Restricts the gateway server listener to 127.0.0.1 and reduces error detail returned to clients.
  • Adds sha256 format validation and extra integrity checking during file downloads; removes CORS from the serve command.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/lib/gateway.ts Adds HTML escaping, security headers, localhost-only binding, and sha256 validation/integrity logic for served content.
src/commands/serve.ts Removes enableCors from serveDir behavior.
.nsite/config.json Adjusts config formatting (indentation change on servers entry).
Comments suppressed due to low confidence (1)

src/commands/serve.ts:52

  • Deno.serve({ port }, handler) defaults to binding on all interfaces. Since this PR is hardening network exposure (and gateway.ts now pins to 127.0.0.1), consider also setting hostname: "127.0.0.1" here so nsyte serve isn't reachable from other devices on the LAN by default.
        });
      };

      // Start server using Deno.serve
      await Deno.serve({ port }, handler).finished;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

const fileList = files.map((file) => {
const size = file.size ? this.formatFileSize(file.size) : "unknown";
return `<li><a href="/${file.path}">${file.path}</a> (${size})</li>`;
return `<li><a href="/${encodeURI(file.path)}">${escapeHtml(file.path)}</a> (${size})</li>`;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The directory listing builds links as href="/${encodeURI(file.path)}". If file.path already starts with / (it appears manifest paths are normalized with a leading /), the resulting href can start with //... (or ///...), which browsers treat as a protocol-relative URL and can send users off-origin. Consider stripping leading slashes before concatenation and encoding path segments so the link is always a same-origin relative path.

Suggested change
return `<li><a href="/${encodeURI(file.path)}">${escapeHtml(file.path)}</a> (${size})</li>`;
const normalizedPath = file.path.replace(/^\/+/, "");
const encodedPath = normalizedPath
.split("/")
.map((segment) => encodeURIComponent(segment))
.join("/");
return `<li><a href="/${encodedPath}">${escapeHtml(file.path)}</a> (${size})</li>`;

Copilot uses AI. Check for mistakes.
Comment on lines 1229 to +1233
const fileSha256 = tryFile.sha256!; // We already checked this is not undefined

// Validate sha256 format to prevent path traversal via cache keys
if (!/^[a-f0-9]{64}$/.test(fileSha256)) {
log.warn(`Invalid sha256 in manifest: ${fileSha256}`);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

fileSha256 is rejected unless it matches /^[a-f0-9]{64}$/. Other tooling/clients may produce uppercase hex; rejecting those manifests will make content unavailable even if the hash is otherwise valid. Consider normalizing with toLowerCase() before validation/comparison, or using a case-insensitive regex.

Suggested change
const fileSha256 = tryFile.sha256!; // We already checked this is not undefined
// Validate sha256 format to prevent path traversal via cache keys
if (!/^[a-f0-9]{64}$/.test(fileSha256)) {
log.warn(`Invalid sha256 in manifest: ${fileSha256}`);
const rawFileSha256 = tryFile.sha256!; // We already checked this is not undefined
const fileSha256 = rawFileSha256.toLowerCase();
// Validate sha256 format to prevent path traversal via cache keys
if (!/^[a-f0-9]{64}$/.test(fileSha256)) {
log.warn(`Invalid sha256 in manifest: ${rawFileSha256}`);

Copilot uses AI. Check for mistakes.
Comment on lines +1319 to +1329
// Verify SHA-256 integrity before caching
const hashBuffer = await crypto.subtle.digest("SHA-256", downloadedData);
const actualHash = [...new Uint8Array(hashBuffer)]
.map((b) => b.toString(16).padStart(2, "0")).join("");
if (actualHash !== fileSha256) {
log.warn(
`Hash mismatch from ${server}: expected ${fileSha256}, got ${actualHash}`,
);
continue; // try next server
}

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

This re-computes and re-checks the SHA-256 of downloadedData even though DownloadService.downloadFromServer() already verifies the sha256 and returns null on mismatch (see src/lib/download.ts). This adds O(n) hashing overhead per download and duplicates logic; consider relying on the DownloadService verification (or centralizing the verification in one place).

Suggested change
// Verify SHA-256 integrity before caching
const hashBuffer = await crypto.subtle.digest("SHA-256", downloadedData);
const actualHash = [...new Uint8Array(hashBuffer)]
.map((b) => b.toString(16).padStart(2, "0")).join("");
if (actualHash !== fileSha256) {
log.warn(
`Hash mismatch from ${server}: expected ${fileSha256}, got ${actualHash}`,
);
continue; // try next server
}

Copilot uses AI. Check for mistakes.
],
"servers": [
"https://blssm.us"
"https://blssm.us"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The servers array entry uses a tab character for indentation, which makes this JSON inconsistent with the surrounding 2-space formatting and can create noisy diffs going forward. Reformat this line to match the existing indentation style.

Suggested change
"https://blssm.us"
"https://blssm.us"

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +58
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; font-src * data:; connect-src 'self' wss: https:; frame-src 'none'; object-src 'none'",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "same-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

securityHeaders()'s CSP uses frame-src 'none', which only controls what this page can embed, not whether other sites can embed this page. For clickjacking protection you likely want frame-ancestors 'none' (and/or X-Frame-Options: DENY).

Suggested change
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; font-src * data:; connect-src 'self' wss: https:; frame-src 'none'; object-src 'none'",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "same-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; font-src * data:; connect-src 'self' wss: https:; frame-src 'none'; frame-ancestors 'none'; object-src 'none'",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "same-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
"X-Frame-Options": "DENY",

Copilot uses AI. Check for mistakes.
function securityHeaders(): Record<string, string> {
return {
"Content-Security-Policy":
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; font-src * data:; connect-src 'self' wss: https:; frame-src 'none'; object-src 'none'",
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The CSP currently includes script-src 'unsafe-inline' and style-src 'unsafe-inline', which significantly reduces the protection CSP provides against XSS. If inline code is required for the injected update script / inline styles, consider switching to nonce- or hash-based CSP (and attaching the nonce to the injected <script>/<style>), or otherwise avoid applying an ineffective CSP.

Suggested change
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; font-src * data:; connect-src 'self' wss: https:; frame-src 'none'; object-src 'none'",
"default-src 'self'; script-src 'self'; style-src 'self'; img-src * data: blob:; media-src * data: blob:; font-src * data:; connect-src 'self' wss: https:; frame-src 'none'; object-src 'none'",

Copilot uses AI. Check for mistakes.
"Content-Type": contentType,
"Content-Length": fileData.byteLength.toString(),
"Cache-Control": "public, max-age=3600", // Browser can cache for 1 hour
...securityHeaders(),
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

securityHeaders() (including the restrictive CSP) is applied to all served nsite files (including user HTML). This can easily break sites that rely on external scripts and may not meaningfully harden XSS while unsafe-inline is allowed. Consider scoping these headers to gateway-generated pages (loading/no-content/directory listing), or making the CSP configurable / opt-out for served site content.

Suggested change
...securityHeaders(),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants