block absolute url 200 rewrites (ssrf), fix __Host-did cookie path, and harden private-site review findings
90 unmodified lines9192939495959697989899100101102103104105106107396 unmodified linesExpand all90 unmodified lines await pruneSessions()}
// Run maintenance on startuprunMaintenance()runMaintenance().catch((err) => logger.error('[Maintenance] Periodic maintenance failed', err))
// Schedule maintenance to run every hoursetInterval(runMaintenance, 60 * 60 * 1000)setInterval( () => { runMaintenance().catch((err) => logger.error('[Maintenance] Periodic maintenance failed', err)) }, 60 * 60 * 1000,)
startPrivateSiteReaper()
// Start DNS verification worker (runs every 10 minutes)396 unmodified linesExpand all232 unmodified linesExpand all2332342352362372372382392402416 unmodified lines2482492502512522532542552562572582522532542552562572581 unmodified line232 unmodified linesExpand all expect(response.headers.get('ETag')).toBe('"test-checksum"') expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable') })
test('proxies absolute 200 rewrite targets with inline query placeholders', async () => { test('rejects absolute 200 rewrite targets without proxying them', async () => { storeFile( '_redirects', '/.well-known/webfinger?resource=:resource https://webfinger.madoka-winter.workers.dev/?resource=:resource 200', 'text/plain',6 unmodified lines 'https://madoka.example/.well-known/webfinger?resource=acct:ana@example.com', { accept: 'application/jrd+json' }, )
expect(response.status).toBe(200) expect(response.headers.get('Content-Type')).toContain('application/jrd+json') expect(await response.json()).toEqual({ subject: 'acct:ana@example.com' }) expect(safeFetchCalls).toHaveLength(1) expect(safeFetchCalls[0]?.url).toBe( 'https://webfinger.madoka-winter.workers.dev/?resource=acct%3Aana%40example.com', ) expect(response.status).toBe(400) expect(await response.text()).toBe('Absolute URL rewrites are not supported') expect(safeFetchCalls).toHaveLength(0) expect(storageGetWithMetadataKeys).not.toContain( `${DID}/${RKEY}/https://webfinger.madoka-winter.workers.dev/?resource=acct%3Aana%40example.com`, ) })1 unmodified line7 unmodified lines891011121213141598 unmodified lines114115116117119120121122123124125118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165128129130131343 unmodified linesExpand all475476477478517479480481482483311 unmodified linesExpand all795796797798837799800801802803283 unmodified linesExpand all7 unmodified linesimport { normalizeFileCids } from '@wispplace/fs-utils'import { isHtmlContent, rewriteHtmlPaths } from '@wispplace/fs-utils/html-rewriter'import type { Record as WispSettings } from '@wispplace/lexicons/types/place/wisp/settings'import { createLogger } from '@wispplace/observability'import { safeFetch } from '@wispplace/safe-fetch'import type { StorageResult } from '@wispplace/tiered-storage'import { lookup } from 'mime-types'import { isSiteUpdating } from './cache-invalidation'import { cache } from './cache-manager'98 unmodified linesfunction isAbsoluteHttpUrl(path: string): boolean { return path.startsWith('http://') || path.startsWith('https://')}
function getForwardedProxyHeaders(requestHeaders?: Record<string, string>): HeadersInit { const forwarded: Record<string, string> = {} for (const name of ['accept', 'accept-language', 'range']) { const value = requestHeaders?.[name] if (value) forwarded[name] = value } return forwardedfunction externalRewriteNotAllowedResponse(targetPath: string): Response { logger.warn('Blocked absolute URL for status 200 rewrite', { targetPath }) return new Response('Absolute URL rewrites are not supported', { status: 400, headers: { 'Cache-Control': 'no-store', }, })}
async function proxyExternalRewrite(targetPath: string, requestHeaders?: Record<string, string>): Promise<Response> { try { const upstream = await safeFetch(targetPath, { headers: getForwardedProxyHeaders(requestHeaders), maxSize: 10 * 1024 * 1024, retry: false, timeout: 30_000, }) const responseHeaders = new Headers(upstream.headers)
responseHeaders.delete('connection') responseHeaders.delete('keep-alive') responseHeaders.delete('proxy-authenticate') responseHeaders.delete('proxy-authorization') responseHeaders.delete('te') responseHeaders.delete('trailer') responseHeaders.delete('transfer-encoding') responseHeaders.delete('upgrade') if (!responseHeaders.has('Cache-Control')) { responseHeaders.set('Cache-Control', STANDARD_CACHE_CONTROL) }
return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: responseHeaders, }) } catch (err) { logger.warn('External rewrite proxy failed', { targetPath, error: err }) return new Response('Bad Gateway', { status: 502, headers: { 'Cache-Control': 'no-store', }, }) }}
function manifestHasPath(fileCids: Record<string, string> | null, filePath: string): boolean { return fileCids === null || fileCids[normalizeFilePath(filePath)] !== undefined}
343 unmodified linesExpand all
// Handle different status codes if (status === 200) { if (isAbsoluteHttpUrl(targetPath)) { const response = await proxyExternalRewrite(targetPath, headers) const response = externalRewriteNotAllowedResponse(targetPath) logTrace(trace, filePath || '/', logger) return response }
311 unmodified linesExpand all
// Handle different status codes if (status === 200) { if (isAbsoluteHttpUrl(targetPath)) { const response = await proxyExternalRewrite(targetPath, headers) const response = externalRewriteNotAllowedResponse(targetPath) logTrace(trace, filePath || '/', logger) return response }
283 unmodified linesExpand all252 unmodified linesExpand all25325425525625725825926026126226326426519 unmodified lines252 unmodified linesExpand all 'idx_private_site_shares_token_hash', async () => db`CREATE INDEX IF NOT EXISTS idx_private_site_shares_token_hash ON private_site_shares(token_hash)`, ], [ 'idx_private_site_shares_token_hash_unique', async () => db`CREATE UNIQUE INDEX IF NOT EXISTS idx_private_site_shares_token_hash_unique ON private_site_shares(token_hash)`, ], [ 'idx_private_sessions_secret', async () => db`CREATE INDEX IF NOT EXISTS idx_private_sessions_secret ON private_site_sessions(secret_hash)`, ], [19 unmodified lines140 unmodified linesExpand all141142143144145146145146147148149150151152153154118 unmodified linesExpand all140 unmodified linesExpand all for (const file of prepared) { await writePrivateFile(site.siteId, file.path, file.bytes, file.mimeType) } } catch (err) { await deletePrivateSiteFiles(site.siteId).catch(() => 0) await deletePrivateSite(site.siteId).catch(() => false) await deletePrivateSiteFiles(site.siteId).catch((cleanupErr) => { logger.error('[PrivateSite] cleanup after failed ingest failed', cleanupErr, { siteId: site.siteId }) }) await deletePrivateSite(site.siteId).catch((cleanupErr) => { logger.error('[PrivateSite] cleanup after failed ingest failed', cleanupErr, { siteId: site.siteId }) }) throw err }
logger.info('[PrivateSite] Created', {118 unmodified linesExpand all93 unmodified lines94959697989910010110277 unmodified lines93 unmodified lines value: session.did, httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 30 * 24 * 60 * 60, // 30 days })
// Revalidate the OAuth state token before returning a share visitor to its site.77 unmodified lines1234567818 unmodified lines272829303031323334353637import type { Context } from 'hono'import { routePath } from 'hono/route'import { logCollector, metricsCollector } from '../core'import { redactSecretPath } from '../redact'
/** * Hono middleware for observability * Tracks request metrics18 unmodified linesexport function observabilityErrorHandler(service: string) { return (err: Error, c: Context) => { const { pathname } = new URL(c.req.url)
logCollector.error(`Request failed: ${c.req.method} ${pathname}`, service, err, { statusCode: c.res.status || 500 }) logCollector.error(`Request failed: ${c.req.method} ${redactSecretPath(pathname)}`, service, err, { statusCode: c.res.status || 500, })
return c.text('Internal Server Error', 500) }}