Skip to content

chore(ci): add fork-side SSO audit script + workflow#23

Closed
awais786 wants to merge 2 commits into
foss-mainfrom
chore/sso-audit-ci
Closed

chore(ci): add fork-side SSO audit script + workflow#23
awais786 wants to merge 2 commits into
foss-mainfrom
chore/sso-audit-ci

Conversation

@awais786
Copy link
Copy Markdown

Summary

Final entry in the cross-app fork-audit family. Mirrors Pressingly/plane#32, Pressingly/twenty#9, Pressingly/outline#20, Pressingly/penpot#19.

SurfSense is architecturally immune to the cross-user stale-session leak class — current_active_user resolves proxy_user (per-request) before falling back to jwt_user. This audit pins that immunity via the regression-guard test introduced in PR #22.

Checks

Row File Invariant Severity
14 surfsense_web/lib/auth-utils.ts SPA logout MUST NOT call /oauth2/sign_out informational
20 tests/unit/test_current_active_user_proxy_precedence.py + app/users.py Precedence test MUST exist AND current_active_user MUST preserve proxy_user > jwt_user order security-critical
21 app/middleware/proxy_auth.py No polynomial-backtracking email-shape regex regression guard

Spec source: sso-rules-moneta/openspec/specs/proxy-auth-middleware/spec.md.

Current state

Local dry-run on foss-main: row 20 ❌ — precedence test doesn't exist yet. PR #22 introduces it. When #22 merges, the audit goes green.

To unblock this PR, rebase onto fix/proxy-auth-stale-session-on-user-switch.

🤖 Generated with Claude Code

Adds a per-fork deterministic audit. Final entry in the cross-app
family — mirrors Pressingly/plane#32, Pressingly/twenty#9,
Pressingly/outline#20, Pressingly/penpot#19.

SurfSense is architecturally immune to the cross-user stale-session
leak class (current_active_user resolves proxy_user before jwt_user).
This audit pins that immunity by checking the regression-guard test
file exists.

Rows covered:
  14 — SPA logout (surfsense_web/lib/auth-utils.ts) MUST NOT call
       /oauth2/sign_out
  20 — surfsense_backend/tests/unit/test_current_active_user_proxy_
       precedence.py MUST exist AND surfsense_backend/app/users.py MUST
       preserve the proxy_user > jwt_user precedence. SECURITY-CRITICAL.
  21 — No polynomial-backtracking email-shape regex (SurfSense uses
       substring check today)

Local dry-run on foss-main:
  row 14 ✅, row 20 ❌ (precedence test missing — #22
  introduces it), row 21 ✅. When #22 merges, the audit goes green.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 16, 2026

SurfSense SSO Fork Audit

Cross-app contract: https://github.com/awais786/sso-rules-moneta/blob/main/openspec/specs/proxy-auth-middleware/spec.md
Row numbers match the 21-row table at https://github.com/awais786/sso-rules-moneta/blob/main/skills/app-rules/SKILL.md#5-report

Row Invariant Status Notes
14 logout shape: SPA logout does not call /oauth2/sign_out surfsense_web/lib/auth-utils.ts does not invoke /oauth2/sign_out (this row verifies only that the SPA doesn't try to clear the upstream proxy cookie itself; that's the portal's job)
20 session-identity reconciliation: proxy>jwt precedence pinned by test surfsense_backend/tests/unit/test_current_active_user_proxy_precedence.py does NOT exist. The proxy_user > jwt_user precedence in surfsense_backend/app/users.py is the load-bearing reason SurfSense is immune to the cross-user stale-session leak — but it's unguarded by tests. A future refactor that flips the precedence (or removes the proxy_user check during a 'simplify auth' pass) would silently re-introduce the bug. Add the regression-guard test from #22.
21 email-shape detection uses substring, not polynomial regex No polynomial-backtracking email-shape regex in surfsense_backend/app/middleware/proxy_auth.py; using substring check

1 violations. Security-critical (row 20): 1.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a CI-only audit (no production code change) that verifies SurfSense's fork-side rows of the cross-app SSO contract from awais786/sso-rules-moneta. A bash script scripts/sso-audit.sh grep-checks three invariants — SPA logout shape, proxy_user > jwt_user precedence in current_active_user (architectural immunity), and absence of a polynomial-backtracking email-shape regex — and a new workflow runs it on PRs touching the relevant paths, on foss-main pushes, weekly, and on demand, publishing results to job summary and a sticky PR comment.

Changes:

  • New scripts/sso-audit.sh implementing checks for rows 14, 20, and 21.
  • New .github/workflows/sso-audit.yml that executes the script, fails on security-critical violations (row 20), and posts a sticky PR comment for same-repo PRs.
  • Audit treats the row-20 precedence test file from PR #22 as the regression-guard pin.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
scripts/sso-audit.sh Bash audit script that grep-checks the three fork-side invariants and prints a markdown summary table.
.github/workflows/sso-audit.yml CI workflow that runs the audit on relevant triggers, surfaces results, and gates merges on security-critical failures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/sso-audit.sh

if grep -qE '/oauth2/sign_?out' "$AUTH_UTILS"; then
local line
line=$(grep -nE '/oauth2/sign_?out' "$AUTH_UTILS" | head -1)
Comment thread scripts/sso-audit.sh
Comment on lines +112 to +128
# The precedence pattern: in current_active_user, proxy_user is checked
# and returned BEFORE jwt_user. We grep for the canonical shape that
# makes the precedence visible.
local has_precedence
has_precedence=$(grep -cE "proxy_user.*is not None|return proxy_user" "$USERS_PY" || true)

local has_test
has_test=0
[[ -f "$PRECEDENCE_TEST" ]] && has_test=1

if [[ "$has_precedence" -gt 0 && "$has_test" -eq 1 ]]; then
record 1 "✅" "$USERS_PY preserves proxy_user > jwt_user precedence (architectural immunity); regression guard test at $PRECEDENCE_TEST pins the contract"
return
fi

if [[ "$has_precedence" -eq 0 ]]; then
record 1 "❌" "$USERS_PY does NOT show the proxy_user precedence pattern. SurfSense's immunity to the cross-user stale-session leak depends on \`current_active_user\` returning \`proxy_user\` before falling back to \`jwt_user\`. If this property is gone, the leak class is re-introduced. Reference: openspec proxy-auth-middleware §'Identity mismatch SHALL flush', surfsense-security.md §'architecturally immune', Pressingly/SurfSense#22."
Comment thread scripts/sso-audit.sh
Comment on lines +144 to +152
local hits
hits=$(grep -nE '\^\[\^[\\\\]s@\]\+@\[\^[\\\\]s@\]\+\\\.\[\^[\\\\]s@\]\+\$' "$MIDDLEWARE" 2>/dev/null || true)

if [[ -n "$hits" ]]; then
record 2 "❌" "Polynomial-backtracking email-shape regex detected in $MIDDLEWARE: $hits. SurfSense uses a substring check (\`'@' not in email\`); introducing the explicit ^...$ regex form re-enables polynomial backtracking on adversarial input. Rewrite to substring or indexOf-based check per openspec proxy-auth-middleware §'email-shape detection SHALL avoid polynomial-backtracking regex'."
return
fi

record 2 "✅" "No polynomial-backtracking email-shape regex in $MIDDLEWARE; using substring check"
Comment thread scripts/sso-audit.sh
Comment on lines +56 to +65
ROW_STATUS[$idx]="$status"
ROW_NOTES[$idx]="$note"
if [[ "$status" == "❌" ]]; then
for c in "${SECURITY_CRITICAL[@]}"; do
if [[ "$c" -eq "$idx" ]]; then
SECURITY_CRITICAL_FAILS=$((SECURITY_CRITICAL_FAILS + 1))
return
fi
done
fi
Comment on lines +13 to +19
paths:
- 'surfsense_backend/app/middleware/proxy_auth.py'
- 'surfsense_backend/app/users.py'
- 'surfsense_backend/tests/unit/test_current_active_user_proxy_precedence.py'
- 'surfsense_web/lib/auth-utils.ts'
- 'scripts/sso-audit.sh'
- '.github/workflows/sso-audit.yml'
Comment on lines +30 to +36
jobs:
sso-fork-audit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

Comment thread .github/workflows/sso-audit.yml Outdated
Comment on lines +26 to +28
permissions:
contents: read
pull-requests: write
Comment on lines +41 to +45
if bash scripts/sso-audit.sh | tee audit-output.md; then
echo "audit_exit=0" >> "$GITHUB_OUTPUT"
else
echo "audit_exit=$?" >> "$GITHUB_OUTPUT"
fi
Scope `pull-requests: write` to the job, not the whole workflow — the audit
step itself only needs `contents: read`. Bump actions/checkout to @v5 to
match the convention used in Pressingly/outline.

Same hardening Pressingly/plane and Pressingly/outline received during
Copilot review; applied preemptively here for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@awais786
Copy link
Copy Markdown
Author

Closing: fork-side audit tooling not needed on this fork. The audit is being maintained in sso-rules-moneta instead, which is the single source of truth across all forks.

@awais786 awais786 closed this May 18, 2026
@awais786 awais786 deleted the chore/sso-audit-ci branch May 18, 2026 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants