fix: reduce alarms further (#3211)

* feat; update bug-bounty

* fix: reduce alarms further
This commit is contained in:
Daniel Salazar
2026-06-04 10:43:50 -07:00
committed by GitHub
parent 3ceba9e3b3
commit 918aba9c80
3 changed files with 60 additions and 15 deletions
+5 -2
View File
@@ -21,13 +21,16 @@ Out-of-scope:
The following have already been reviewed and determined **not to be vulnerabilities**. Reports that only re-describe one of these are **not eligible for a reward** and will be closed as non-issues — even if they include new code references. Please review this list before submitting:
* **XSS / CORS / token issues scoped to `api.puter.com`.** The API origin holds no sensitive session cookies; the user session lives on `puter.com`.
* **XSS / CORS / token issues scoped to `api.puter.com`.** The primary user session cookie lives on `puter.com`, not on the API origin, so reflected/stored XSS, CORS, or token handling on `api.puter.com` is generally out of scope. Two exceptions we *do* evaluate on their merits — please report these: (a) attacker-controlled content served **inline** (e.g. an HTML `Content-Type`) from API file/response endpoints, and (b) anything that abuses the app-scoped `puter_token_v2` companion cookie set on the API host.
* **SSRF via `secureFetch`.** Production routes outbound requests through an isolated proxy that has no access to internal/SSRF-sensitive resources.
* **Attacks that depend on guessing an `appInstanceID` or app UID.** These are random 128-bit secret values and are not considered guessable.
* **Apps invoking drivers, creating workers, or using KV.** Applications are intended to do this; worker permissions are scoped to the owning app. This is by design.
* **App metadata or app user-count "leaks".** This information is currently public by design.
* **General "token in a URL" / token-lifetime designs** — signed directory URLs exposing children, a write signature implying read, or app tokens outliving a web session. These are current intended behaviors.
* **Missing PKCE or other OIDC hardening** where the provider's token is already verified over TLS. Please open a GitHub issue/PR for hardening suggestions.
* **Missing PKCE, unverified `id_token` signatures, or other OIDC hardening.** Provider tokens are obtained through a server-to-server authorization-code exchange with the provider's token endpoint over TLS, so the resulting `id_token` / userinfo claims are trusted from that channel rather than from local JWKS signature verification — the callback never accepts a caller-supplied `id_token`. Adding local signature / `aud` / `iss` / `exp` checks is welcome defense-in-depth (please open a GitHub issue/PR), but their absence is not an account-takeover vector on its own.
* **JWT "algorithm confusion" / unpinned `algorithms` in `jwt.verify`.** Puter's session tokens are HMAC-signed (HS256) with a server-side secret, and there is no asymmetric public key anywhere in the verification path, so `alg`-substitution attacks do not apply. Explicitly pinning `algorithms` is a fine hardening PR, but it is not a vulnerability.
* **Static-source "SQL injection" in internal pagination.** Findings such as `LIMIT ${limit}` in list/notification queries: the limit is numerically coerced and clamped before it reaches the query, so it is not reachable with attacker-controlled string input. Hardening PRs to the internal stores are welcome, but these are not exploitable as reported.
* **Deprecated `saveTo*` GUI app messages.** The legacy `saveToDesktop` / `saveToDocuments` / etc. app-IPC handlers can create — never overwrite — new files in standard user folders. The behavior is non-destructive, path-traversal-safe, and deprecated (slated for removal); it is not treated as a sandbox escape.
* **Best-practice suggestions** such as login/registration username enumeration (kept intentionally for UX) or unauthenticated unsubscribe links (industry norm).
* **Rate-limiting suggestions for TURN credential issuance** (intentional; not billed per tunnel).
@@ -337,7 +337,7 @@ describe('MeteringService', () => {
const alarmSpy = vi.spyOn(server.clients.alarm, 'create');
// Previous usage was 0 (under the allowance) — one big request that
// blows past the limit is legitimate and shouldn't page.
// blows straight past several multiples is legitimate, not abuse.
await target.incrementUsage(
bigActor,
'ai:chat',
@@ -349,11 +349,11 @@ describe('MeteringService', () => {
alarmSpy.mockRestore();
});
it('alarms on the next expense once already at or past the limit', async () => {
it('does not alarm on further usage past the limit until the next multiple is crossed', async () => {
const overActor: Actor = { user: makeUser() };
const sub = await target.getActorSubscription(overActor);
// First expense takes them exactly to the limit — no alarm yet.
// Take them just over the allowance (into the 1x2x band).
await target.incrementUsage(
overActor,
'ai:chat',
@@ -361,11 +361,36 @@ describe('MeteringService', () => {
sub.monthUsageAllowance,
);
// Spy only on the *second* expense, which lands while already at
// the limit — this is the one that should flag.
// A small further expense stays within the same band — no new
// multiple crossed, so it shouldn't page.
const alarmSpy = vi.spyOn(server.clients.alarm, 'create');
await target.incrementUsage(overActor, 'ai:chat', 1, 1_000);
expect(wasOveruseAlarmed(alarmSpy)).toBe(false);
alarmSpy.mockRestore();
});
it('alarms when a whole multiple of the allowance is crossed while already over', async () => {
const overActor: Actor = { user: makeUser() };
const sub = await target.getActorSubscription(overActor);
// First expense takes them to the limit (1x) — no alarm yet.
await target.incrementUsage(
overActor,
'ai:chat',
1,
sub.monthUsageAllowance,
);
// Spy only on the expense that crosses into 2x while already over.
const alarmSpy = vi.spyOn(server.clients.alarm, 'create');
await target.incrementUsage(
overActor,
'ai:chat',
1,
sub.monthUsageAllowance,
);
expect(alarmSpy).toHaveBeenCalledWith(
expect.stringContaining('usage exceeded'),
expect.stringContaining('exceeded their usage allowance'),
@@ -377,8 +402,12 @@ describe('MeteringService', () => {
it('does not alarm while purchased credits still cover the overage', async () => {
const creditActor: Actor = { user: makeUser() };
const sub = await target.getActorSubscription(creditActor);
await target.updateAddonCredit(creditActor.user.uuid, 5_000_000);
await target.updateAddonCredit(
creditActor.user.uuid,
5_000_000_000,
);
// Cross to 2x — would page if not for the credits covering it.
await target.incrementUsage(
creditActor,
'ai:chat',
@@ -387,7 +416,12 @@ describe('MeteringService', () => {
);
const alarmSpy = vi.spyOn(server.clients.alarm, 'create');
await target.incrementUsage(creditActor, 'ai:chat', 1, 1_000);
await target.incrementUsage(
creditActor,
'ai:chat',
1,
sub.monthUsageAllowance,
);
expect(wasOveruseAlarmed(alarmSpy)).toBe(false);
alarmSpy.mockRestore();
@@ -897,18 +897,26 @@ export class MeteringService extends PuterService {
// No metered allowance to exceed (e.g. unlimited policies) — nothing to flag.
if (!(allowance > 0)) return;
// Only alarm if the actor was ALREADY at or past their allowance before
// this expense arrived. A single large request that crosses the limit in
// one shot (previous usage still under the allowance) is legitimate and
// shouldn't page — we only flag sustained overuse, i.e. the next expense
// that lands while they're already over.
const previousUsage = actorUsages.total - incrementCost;
const allowedMultiple = Math.floor(actorUsages.total / allowance);
const previousMultiple = Math.floor(previousUsage / allowance);
// Only alarm if the actor was ALREADY at or past their allowance before
// this expense arrived. A single large request that jumps past the limit
// in one shot (previous usage still under the allowance) is legitimate
// and shouldn't page.
const wasAlreadyOverLimit = previousUsage >= allowance;
// And only when this expense crosses into a new whole multiple of the
// allowance (2x, 3x, …) rather than on every expense once over — that
// first-over multiple is 2x, since being already over means the previous
// multiple was at least 1.
const crossedMultiple = previousMultiple < allowedMultiple;
const hasNoAddonCredit =
(actorAddons.purchasedCredits || 0) <=
(actorAddons.consumedPurchaseCredits || 0);
if (!(wasAlreadyOverLimit && hasNoAddonCredit)) return;
if (!(wasAlreadyOverLimit && crossedMultiple && hasNoAddonCredit))
return;
this.clients.alarm.create(
`metering usage exceeded by user: ${actor.user?.username}`,