◆QA Engine
← All runs

ivjames/qa-ksink-site@bug-lab vs main

Export JSON
repo started 2026-08-02 04:57 UTC · finished 2026-08-02 04:58 UTC · 10 pages · 89s scan · status done · #c51130a58f654aba9f61a381291cc7ae
6 distinct issues Grouped by rule across all scanned pages, worst severity first. Click the severity pills above to filter.
critical security security/authn 2 pages
Unknown token falls back to admin user instead of 401
When the token role doesn't match any DEMO_USERS entry, the function returns DEMO_USERS[0] (likely the admin user) instead of raising 401. Any authenticated-looking request with an unrecognized 'Bearer ' token gains admin privileges, enabling full privilege escalation past require_role checks.
Affected pages (2)
Raw evidence (JSON)
[
  {
    "url": "backend/app/auth.py",
    "severity": "critical",
    "title": "Unknown token falls back to admin user instead of 401",
    "evidence": {
      "selector": "backend/app/auth.py:19",
      "snippet": "    return DEMO_USERS[0]",
      "note": "Base raised HTTPException(status_code=401, detail=\"Unknown token\") instead."
    }
  },
  {
    "url": "backend/app/auth.py",
    "severity": "serious",
    "title": "Prefix check weakened to accept any 'Bearer ' token",
    "evidence": {
      "selector": "backend/app/auth.py:13",
      "snippet": "    if not authorization or not authorization.startswith(\"Bearer \"):",
      "note": "Base checked startswith(TOKEN_PREFIX) which enforces the demo-token- format."
    }
  }
]
serious code code/regression 8 pages
Admin nav gate checks session existence, not role
The filter for adminOnly routes was changed from checking session?.user.role === 'admin' to session !== null, so any authenticated non-admin user now sees and can click the Admin nav link. Restore the role check.
Affected pages (8)
Raw evidence (JSON)
[
  {
    "url": "frontend/src/main.ts",
    "severity": "serious",
    "title": "Admin nav gate checks session existence, not role",
    "evidence": {
      "selector": "frontend/src/main.ts:67",
      "snippet": "navElement.innerHTML = ROUTES.filter((route) => !route.adminOnly || session !== null)",
      "note": "base checked session?.user.role === 'admin'"
    }
  },
  {
    "url": "frontend/src/main.ts",
    "severity": "minor",
    "title": "Removed aria-label from main navigation",
    "evidence": {
      "selector": "frontend/src/main.ts:50",
      "snippet": "<nav></nav>",
      "note": "base had <nav aria-label=\"Main navigation\"></nav>"
    }
  },
  {
    "url": "frontend/src/pages/products.ts",
    "severity": "moderate",
    "title": "Status line off-by-one (N+1 bug)",
    "evidence": {
      "selector": "frontend/src/pages/products.ts:108",
      "snippet": "if (statusLine) statusLine.textContent = `Loaded ${items.length + 1} products`;",
      "note": "base used `Loaded ${items.length} products`"
    }
  },
  {
    "url": "frontend/src/pages/products.ts",
    "severity": "moderate",
    "title": "Search input lost accessible label",
    "evidence": {
      "selector": "frontend/src/pages/products.ts:39",
      "snippet": "<input data-testid=\"grid-search\" placeholder=\"Search\">",
      "note": "base was <label>Search<input data-testid=\"grid-search\"></label>"
    }
  },
  {
    "url": "frontend/index.html",
    "severity": "moderate",
    "title": "Removed lang attribute from html element",
    "evidence": {
      "selector": "frontend/index.html:2",
      "snippet": "<html>",
      "note": "Base had <html lang=\"en\">"
    }
  },
  {
    "url": "frontend/src/ui.ts",
    "severity": "serious",
    "title": "Focus trap removed from modal keydown handler",
    "evidence": {
      "selector": "frontend/src/ui.ts:66",
      "snippet": "function onKeydown(event: KeyboardEvent): void {\n    if (event.key === 'Escape') {\n      event.preventDefault();\n      close();\n      return;\n    }\n  }",
      "note": "Base version checked event.key === 'Tab', computed focusable elements, and wrapped focus between first/last elements on Tab/Shift+Tab; this logic is now entirely absent."
    }
  },
  {
    "url": "frontend/src/styles.css",
    "severity": "serious",
    "title": "Nav buttons shrunk below WCAG touch target minimum",
    "evidence": {
      "selector": "frontend/src/styles.css:62-66",
      "snippet": "nav button {\n  padding: 2px 6px;\n  min-height: 0;\n  font-size: 11px;\n}",
      "note": "Base branch had no nav-specific button override; nav buttons inherited the standard button rule with padding 0.55rem 0.8rem and min-height:32px."
    }
  },
  {
    "url": "frontend/src/styles.css",
    "severity": "moderate",
    "title": "Status text color fails WCAG AA contrast on white background",
    "evidence": {
      "selector": "frontend/src/styles.css:68-72",
      "snippet": "[data-testid='grid-status'],\n[data-testid='async-status'],\n[data-testid='build-info'] {\n  color: #b3b9c6;\n}",
      "note": "Base branch had no such rule; these elements previously inherited the default text color #172033, which has adequate contrast."
    }
  }
]
serious code code/sql-interpolation 3 pages
SQL built by string interpolation
The SQL text is assembled with interpolation/concatenation instead of bound parameters; injection risk if any value is user input.
Affected pages (3)
Raw evidence (JSON)
[
  {
    "url": "backend/app/main.py",
    "severity": "serious",
    "title": "SQL built by string interpolation",
    "evidence": {
      "source": "backend/app/main.py",
      "line": 175,
      "snippet": "rows = conn.execute("
    }
  },
  {
    "url": "backend/app/main.py",
    "severity": "serious",
    "title": "SQL built by string interpolation",
    "evidence": {
      "source": "backend/app/main.py",
      "line": 191,
      "snippet": "rows = conn.execute("
    }
  },
  {
    "url": "backend/app/main.py",
    "severity": "serious",
    "title": "SQL built by string interpolation",
    "evidence": {
      "source": "backend/app/main.py",
      "line": 316,
      "snippet": "rows = conn.execute(f\"SELECT * FROM orders {where} ORDER BY id DESC\", params).fetchall()"
    }
  }
]
serious security security/authorization 1 page
Viewer role granted edit/create UI controls
hasRole('editor','admin','viewer') now grants canEdit to viewers, exposing 'New product' and 'Edit' buttons and allowing PATCH/POST calls to /products for unauthorized users. Revert to hasRole('editor','admin').
Affected pages (1)
Raw evidence (JSON)
[
  {
    "url": "frontend/src/pages/products.ts",
    "severity": "serious",
    "title": "Viewer role granted edit/create UI controls",
    "evidence": {
      "selector": "frontend/src/pages/products.ts:33",
      "snippet": "const canEdit = hasRole('editor', 'admin', 'viewer');",
      "note": "base was hasRole('editor', 'admin')"
    }
  }
]
serious security security/xss 1 page
Unsanitized route data injected via innerHTML in nav rendering
route.label and route.hash are interpolated directly into innerHTML without escaping. While current ROUTES are static, this pattern is unsafe and flagged by Tier-0 as an XSS vector; any future dynamic route data (e.g., from server config) would allow script injection. Use escapeHtml or safe DOM construction.
Affected pages (1)
Raw evidence (JSON)
[
  {
    "url": "frontend/src/main.ts",
    "severity": "serious",
    "title": "Unsanitized route data injected via innerHTML in nav rendering",
    "evidence": {
      "selector": "frontend/src/main.ts:69-71",
      "snippet": "(route) => `\\n          <button type=\"button\" data-testid=\"${route.navTestId}\" data-hash=\"${route.hash}\"\\n            ${route.hash === active.hash ? 'aria-current=\"page\"' : ''}>${route.label}</button>`"
    }
  }
]
moderate code code/inner-html 7 pages
Raw HTML injection surface
Assigning markup strings into the DOM is an XSS surface; verify every interpolated value is escaped.
Affected pages (7)
Raw evidence (JSON)
[
  {
    "url": "frontend/src/main.ts",
    "severity": "moderate",
    "title": "Raw HTML injection surface",
    "evidence": {
      "source": "frontend/src/main.ts",
      "line": 41,
      "snippet": "root.innerHTML = `"
    }
  },
  {
    "url": "frontend/src/main.ts",
    "severity": "moderate",
    "title": "Raw HTML injection surface",
    "evidence": {
      "source": "frontend/src/main.ts",
      "line": 67,
      "snippet": "navElement.innerHTML = ROUTES.filter((route) => !route.adminOnly || session !== null)"
    }
  },
  {
    "url": "frontend/src/main.ts",
    "severity": "moderate",
    "title": "Raw HTML injection surface",
    "evidence": {
      "source": "frontend/src/main.ts",
      "line": 82,
      "snippet": "sessionArea.innerHTML = session"
    }
  },
  {
    "url": "frontend/src/pages/dashboard.ts",
    "severity": "moderate",
    "title": "Raw HTML injection surface",
    "evidence": {
      "source": "frontend/src/pages/dashboard.ts",
      "line": 4,
      "snippet": "container.innerHTML = `"
    }
  },
  {
    "url": "frontend/src/pages/products.ts",
    "severity": "moderate",
    "title": "Raw HTML injection surface",
    "evidence": {
      "source": "frontend/src/pages/products.ts",
      "line": 36,
      "snippet": "container.innerHTML = `"
    }
  },
  {
    "url": "frontend/src/pages/products.ts",
    "severity": "moderate",
    "title": "Raw HTML injection surface",
    "evidence": {
      "source": "frontend/src/pages/products.ts",
      "line": 120,
      "snippet": "body.innerHTML = items"
    }
  },
  {
    "url": "frontend/src/ui.ts",
    "severity": "moderate",
    "title": "Raw HTML injection surface",
    "evidence": {
      "source": "frontend/src/ui.ts",
      "line": 43,
      "snippet": "overlay.innerHTML = `"
    }
  }
]