Skip to content

[finding] a bare-Error refusal reaching queryDataset is classified by WORDING — a security refusal whose text happens to contain "not registered" becomes a 200 with an empty chart #17335

Description

@os-tesla

⚠️ Re-filed. The original card (#17130) was removed with the os-trump account when it was suspended, together with its dispatch comment and the os-dev-report. The delivered branch survived and is now PR #17336.

⛔ Correction to this body's first revision: it named the delivering PR as #17335, which is this card's own number. The PR is #17336.

Filed from PR #17125's out_of_scope_findings (card #16918), and verified on origin/main 71629a151 rather than taken from the report.

The mechanism, measured

packages/services/service-analytics/src/analytics-service.ts wraps dataset execution and, on a throw, decides between re-raising and degrading to an empty result:

} catch (err) {
  if (hasDeclaredErrorEnvelope(err)) throw err;   // the escape hatch
  if (isMissingSourceError(err)) {                // else, judge by WORDING
                                                 // → a warn and a confident empty chart

hasDeclaredErrorEnvelope is true only when the error carries both status: number and a non-empty code: string. A bare Error carries neither, so it falls through to:

function isMissingSourceError(err: unknown): boolean {
  const raw = String((err as { message?: unknown })?.message ?? err ?? '');
  if (isMissingColumnOfRelation(raw)) return false;
  const msg = raw.toLowerCase();
  return (
    msg.includes('no such table') ||
    /relation\s+\s+does not exist/i.test(raw) ||
    msg.includes("doesn't exist") ||
    msg.includes('not registered') ||          // ⇐
    msg.includes('unknown object') ||          // ⇐
    msg.includes('is not a registered object') // ⇐
  );
}

Any bare Error whose message happens to contain one of those substrings is reclassified as "the source table is gone" and served to the caller as an empty chart — no exception, no 4xx, no 5xx. The file's own docblock names that outcome as the #5033 symptom it exists to prevent: "the caller's own mistake reported as 'no data', with no exception … just a warn and a confident empty chart."

⭐ What makes it security-shaped

PR #17125 (card #16918) makes the row-scope bridge refuse the query when a wired security service is broken — a throw new Error('[Analytics] row-level read scope could not be resolved for "X"; query refused (fail-closed).').

That is a bare Error. It propagates today only because its wording happens to match none of the six substrings. ⚠️ ⛔ That is a coincidence, not a construction. The three underlined limbs are exactly the phrasings a registry or security refusal naturally reaches for. ⇒ the next security refusal written in this package, or the next reword of this one, can silently become a 200 with no rows — a fail-closed gate turned back into a fail-open one by string matching.

Not a defect in PR #17125. Its text was measured and today's behaviour is correct. What is defective is that the correctness rests on wording.

Fences

  • Relaxing a security boundary is the maintainer's floor. Every direction here tightens or is neutral. If a proposal would make any refusal more likely to degrade, stop and report.
  • ⛔ Do not widen isMissingSourceError's subtraction list by hand-adding this one message. That fixes one string, not the property.
  • ⛔ Do not reword PR fix(service-analytics): row-scope bridge tells absent from broken security service #17125's refusal to dodge the problem: the job is to remove the coincidence, not to pick a luckier string.

Delivered

PR #17336 (branch claude/issue-17130-declared-envelope-on-analytics-refusals-r2, head d1cd397d4). Two legs:

  1. A guard whose population is derived from source (TypeScript AST over every throw in the package), asserting no bare refusal this package raises can match isMissingSourceError.
  2. readScopeUnresolvedError stamps the already-registered READ_SCOPE_COMPILE_FAILED / 500 on both row-scope resolution refusals, so they short-circuit at hasDeclaredErrorEnvelope instead of being sniffed. No ledger row added ⇒ Clause-②: no.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions