| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 6659441 commit 5029348
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -40,6 +40,16 @@ const b64url = (buf) => Buffer.from(buf).toString('base64url'); | |||
| 40 | 40 | * only this plugin verifies them, so a stolen session token buys markets | |
| 41 | 41 | * access and nothing else on the pod. | |
| 42 | 42 | */ | |
| 43 | + // A session-signing key that was committed to this repo's history by a | ||
| 44 | + // test writing into the source tree. Removing the file from the working | ||
| 45 | + // tree is NOT remediation: the blob is still `git show`-able, so anyone | ||
| 46 | + // with the repo could forge a session token for any agent on any | ||
| 47 | + // deployment whose pluginDir was seeded from those commits. Refusing to | ||
| 48 | + // boot on it is the only mitigation a plugin can enforce by itself. | ||
| 49 | + const BURNED_SECRETS = new Set([ | ||
| 50 | + 'c59b6df7144548b73d07746635c6774828bf8cc74af8febb8d2ce5db2699d277', | ||
| 51 | + ]); | ||
| 52 | + | ||
| 43 | 53 | export function createSessions({ dir, ttlMs }) { | |
| 44 | 54 | const secretFile = path.join(dir, 'session.secret'); | |
| 45 | 55 | let secret; | |
@@ -49,6 +59,13 @@ export function createSessions({ dir, ttlMs }) { | |||
| 49 | 59 | secret = crypto.randomBytes(32); | |
| 50 | 60 | fs.writeFileSync(secretFile, secret, { mode: 0o600 }); | |
| 51 | 61 | } | |
| 62 | + if (BURNED_SECRETS.has(crypto.createHash('sha256').update(secret).digest('hex'))) { | ||
| 63 | + throw new Error( | ||
| 64 | + `markets: ${secretFile} is a key that leaked into git history — anyone with the repository ` | ||
| 65 | + + 'can forge session tokens for any agent. Delete the file (a fresh key is generated on the ' | ||
| 66 | + + 'next boot; every existing session is invalidated, which is the point).', | ||
| 67 | + ); | ||
| 68 | + } | ||
| 52 | 69 | ||
| 53 | 70 | const sign = (payload) => crypto.createHmac('sha256', secret).update(payload).digest(); | |
| 54 | 71 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -261,18 +261,32 @@ export async function activate(api) { | |||
| 261 | 261 | return claims.agent; | |
| 262 | 262 | } | |
| 263 | 263 | ||
| 264 | - async function resolveAgent(request) { | ||
| 264 | + /** | ||
| 265 | + * @returns {Promise<{agent:string|null, ambient:boolean}>} `ambient` is | ||
| 266 | + * true when the credential is one a browser attaches by itself — a | ||
| 267 | + * cookie, or a WebID-TLS client certificate — i.e. one a cross-origin | ||
| 268 | + * page could borrow without knowing it. | ||
| 269 | + */ | ||
| 270 | + async function resolveAgentFull(request) { | ||
| 265 | 271 | const cookie = cookieToken(request); | |
| 266 | 272 | if (cookie) { | |
| 267 | 273 | const agent = liveSession(cookie); | |
| 268 | - if (agent) return agent; | ||
| 274 | + if (agent) return { agent, ambient: true }; | ||
| 269 | 275 | } | |
| 270 | 276 | const auth = request.headers.authorization; | |
| 271 | 277 | if (auth && auth.startsWith('Bearer v1.')) { | |
| 272 | 278 | const agent = liveSession(auth.slice(7)); | |
| 273 | - if (agent) return agent; | ||
| 279 | + if (agent) return { agent, ambient: false }; | ||
| 274 | 280 | } | |
| 275 | - return api.auth.getAgent(request); | ||
| 281 | + const agent = await api.auth.getAgent(request); | ||
| 282 | + // getAgent may have authenticated from an ambient TLS client | ||
| 283 | + // certificate; only an explicit Authorization header proves the | ||
| 284 | + // caller actually held a secret. | ||
| 285 | + return { agent, ambient: agent ? !auth : false }; | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + async function resolveAgent(request) { | ||
| 289 | + return (await resolveAgentFull(request)).agent; | ||
| 276 | 290 | } | |
| 277 | 291 | ||
| 278 | 292 | // ------------------------------------------------------------ replies | |
@@ -309,9 +323,9 @@ export async function activate(api) { | |||
| 309 | 323 | async function authed(request, reply, { mutating = true } = {}) { | |
| 310 | 324 | // Resolve first so an anonymous caller gets a 401 rather than a | |
| 311 | 325 | // confusing 403; nothing is acted on before the CSRF check below. | |
| 312 | - const agent = await resolveAgent(request); | ||
| 326 | + const { agent, ambient } = await resolveAgentFull(request); | ||
| 313 | 327 | if (!agent) { err(reply, 401, 'authentication required'); return null; } | |
| 314 | - if (mutating && !csrfOk(request)) { | ||
| 328 | + if (mutating && ambient && !isSameOrigin(request, originFor(request))) { | ||
| 315 | 329 | err(reply, 403, 'cross-origin request refused — this endpoint is same-origin only'); | |
| 316 | 330 | return null; | |
| 317 | 331 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -407,9 +407,17 @@ export function createStore({ dir, log, prices }) { | |||
| 407 | 407 | * ledger ahead of the durable one (the old design mutated first and | |
| 408 | 408 | * 500'd after, leaving memory and disk permanently divergent). | |
| 409 | 409 | */ | |
| 410 | + function reopenJournal() { | ||
| 411 | + jfd = fs.openSync(journalFile, 'a'); | ||
| 412 | + } | ||
| 413 | + | ||
| 410 | 414 | function commit(ev) { | |
| 411 | 415 | ev.seq = state.seq + 1; | |
| 412 | 416 | ev.t = ev.t || Date.now(); | |
| 417 | + // A descriptor lost during rotation (or by anything else) must not | ||
| 418 | + // wedge the ledger forever: recover it here rather than failing | ||
| 419 | + // every trade and settlement from now on. | ||
| 420 | + if (jfd === null) reopenJournal(); | ||
| 413 | 421 | const line = Buffer.from(`${JSON.stringify(ev)}\n`, 'utf8'); | |
| 414 | 422 | let off = 0; | |
| 415 | 423 | while (off < line.length) off += fs.writeSync(jfd, line, off, line.length - off); | |
@@ -443,7 +451,11 @@ export function createStore({ dir, log, prices }) { | |||
| 443 | 451 | // forever. Always get a working descriptor back. | |
| 444 | 452 | log.error(`markets: journal rotation failed: ${err.message}`); | |
| 445 | 453 | } finally { | |
| 446 | - jfd = fs.openSync(journalFile, 'a'); | ||
| 454 | + // If THIS throws, jfd stays null and commit() reopens lazily. | ||
| 455 | + jfd = null; | ||
| 456 | + try { reopenJournal(); } catch (err) { | ||
| 457 | + log.error(`markets: could not reopen the journal after rotation: ${err.message}`); | ||
| 458 | + } | ||
| 447 | 459 | } | |
| 448 | 460 | log.info(`markets: rotated journal at seq ${state.seq} (prior segment retained for audit)`); | |
| 449 | 461 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -59,6 +59,7 @@ export function renderUi(prefix) { | |||
| 59 | 59 | .status{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em} | |
| 60 | 60 | .status.open{color:var(--up)} .status.closed{color:var(--ink3)} | |
| 61 | 61 | .status.resolving{color:var(--down)} .status.disputed{color:var(--bad)} | |
| 62 | + .status.voiding{color:var(--down)} | ||
| 62 | 63 | .status.resolved{color:var(--accent)} .status.void{color:var(--bad)} | |
| 63 | 64 | table{width:100%;border-collapse:collapse;font-size:.85rem} | |
| 64 | 65 | td,th{padding:.45rem .4rem;border-bottom:1px solid var(--line);text-align:left} | |
@@ -530,6 +531,8 @@ export function renderUi(prefix) { | |||
| 530 | 531 | $('d-meta').innerHTML = '<span class="status ' + esc(m.status) + '">' + esc(m.status) + '</span>' | |
| 531 | 532 | + (m.status === 'resolved' && m.resolvedOutcome != null ? ' → <b>' + esc(m.outcomes[m.resolvedOutcome]) + '</b>' : '') | |
| 532 | 533 | + (m.status === 'resolving' ? ' → <b>' + esc(m.outcomes[m.resolvedOutcome]) + '</b> · settles ' + new Date(m.settleAt).toLocaleTimeString() + ' (disputable)' : '') | |
| 534 | + + (m.status === 'voiding' ? ' → <b>void proposed</b> · settles ' + new Date(m.settleAt).toLocaleTimeString() | ||
| 535 | + + ' — everyone is refunded at most what they paid, nobody wins (disputable)' : '') | ||
| 533 | 536 | + ' · <span id="d-countdown">' + (m.tradable ? countdown(m.closesAt) | |
| 534 | 537 | : 'closed ' + new Date(m.closesAt).toLocaleString()) + '</span>' | |
| 535 | 538 | + ' · pool ' + cr(m.liquidity) + ' · ' + m.trades + ' trades' | |
@@ -590,13 +593,15 @@ export function renderUi(prefix) { | |||
| 590 | 593 | const isOracle = me && (me.agent === m.oracle); | |
| 591 | 594 | $('oracle-row').classList.toggle('hidden', !(isOracle && m.canResolve)); | |
| 592 | 595 | $('o-outcome').innerHTML = m.outcomes.map((o, i) => '<option value="' + i + '">' + esc(o) + '</option>').join(''); | |
| 593 | - const canDispute = m.status === 'resolving' && pos; | ||
| 596 | + const canDispute = (m.status === 'resolving' || m.status === 'voiding') && pos; | ||
| 594 | 597 | $('dispute-row').classList.toggle('hidden', !canDispute); | |
| 595 | 598 | if (canDispute) { | |
| 596 | 599 | // State the bond, the stakes and the deadline BEFORE taking money. | |
| 597 | 600 | const bond = Math.max(25, pos.totalCost * 0.2); | |
| 598 | - $('dispute-copy').innerHTML = 'This market resolved as <b>' + esc(m.outcomes[m.resolvedOutcome]) | ||
| 599 | - + '</b>. Disputing stakes a bond of about <b>' + cr(bond) + ' credits</b>, which you ' | ||
| 601 | + $('dispute-copy').innerHTML = (m.status === 'voiding' | ||
| 602 | + ? 'The oracle has proposed to <b>void</b> this market — nobody wins and you get back at most what you paid. ' | ||
| 603 | + : 'This market resolved as <b>' + esc(m.outcomes[m.resolvedOutcome]) + '</b>. ') | ||
| 604 | + + 'Disputing stakes a bond of about <b>' + cr(bond) + ' credits</b>, which you ' | ||
| 600 | 605 | + '<b>lose</b> unless an operator agrees with you. ' | |
| 601 | 606 | + (m.disputes ? m.disputes + ' dispute(s) already filed. ' : '') | |
| 602 | 607 | + 'If nobody adjudicates in time, the resolution stands.'; | |
| Back | FazBrowse Home | New Git URL |
0 commit comments