Skip to content

fix(auth): reject SQL LIKE metacharacters in ForwardAuth email claim#18

Merged
awais786 merged 1 commit into
foss-mainfrom
fix/forwardauth-wildcard-injection
May 13, 2026
Merged

fix(auth): reject SQL LIKE metacharacters in ForwardAuth email claim#18
awais786 merged 1 commit into
foss-mainfrom
fix/forwardauth-wildcard-injection

Conversation

@awais786
Copy link
Copy Markdown

@awais786 awais786 commented May 13, 2026

Summary

Closes a SQL LIKE wildcard match in the ForwardAuth (AUTH_TYPE=SSO) branch of validateAuthentication by switching the user-lookup from Op.iLike to exact match.

Root cause

The lookup used User.findOne({ where: { email: { [Op.iLike]: email } } }). ILIKE treats % and _ in the supplied value as wildcards, so x-auth-request-email: %@%.% produces ILIKE '%@%.%' which matches every row. The first matched user (typically the bootstrap admin) is then issued a 3-month JWT cookie.

Fix

   user = await User.scope("withTeam").findOne({
-    where: {
-      email: { [Op.iLike]: email },
-    },
+    where: { email },
   });

Exact match strips all special meaning from % and _ — they become ordinary characters and where: { email: "%@%.%" } finds nobody. Email is already .toLowerCase().trim()'d before the lookup, matching the existing User.findByEmail pattern; emails are stored canonically lowercased so case-insensitive matching isn't required.

Reachability in production

Verified the bug is not exploitable from the public internet against docs.foss.arbisoft.com:

  • POST /api/auth.info with x-auth-request-email: %@%.% and no session returns HTTP/2 302 redirecting to Cognito. oauth2-proxy intercepts unauthenticated requests before they reach Outline, and overwrites x-auth-request-* headers from the verified OIDC identity for authenticated requests.

This is a defense-in-depth fix at the code level. If the backend is ever directly reachable (k8s NetworkPolicy change, dev environment, internal pivot) or oauth2-proxy is replaced, the code is no longer the failure point.

Test plan

  • yarn test server/middlewares/authentication.test.ts — new regression test asserts wildcard input does not impersonate an existing user; all existing ForwardAuth tests still pass (Cognito numeric IDs, real emails, bare-username + DEFAULT_EMAIL_DOMAIN coercion).
  • Manual smoke: log in via oauth2-proxy with a Cognito user → still works.

Notes / out of scope

  • Wildcard input still falls through to user-provisioning (creates a junk user row). That path is unreachable in prod through oauth2-proxy and is harmless without admin privileges; can be tightened in a follow-up if internal reachability becomes a concern.
  • ForwardAuth-minted access-token cookie still has a 3-month TTL outliving IdP-side revocation — separate follow-up.

The ForwardAuth (AUTH_TYPE=SSO) branch of validateAuthentication looked
users up with User.findOne({ where: { email: { [Op.iLike]: email } } }).
ILIKE treats % and _ in the supplied value as wildcards, so a request
with "x-auth-request-email: %@%.%" produces ILIKE '%@%.%' — which
matches every row. The first matched user (typically the bootstrap
admin) was then issued a 3-month JWT cookie.

Switching to exact match (where: { email }) strips all special meaning
from those characters. Email is already .toLowerCase().trim()'d before
the lookup, matching the existing User.findByEmail pattern; emails are
stored canonically lowercased so case-insensitive matching is not
required.

Not exploitable in production: oauth2-proxy intercepts unauthenticated
requests before they reach Outline and overwrites x-auth-request-*
headers from the verified OIDC identity. Defense-in-depth fix at the
code level in case backend reachability ever changes.

Adds a regression test asserting wildcard input does not impersonate
an existing user.
@awais786 awais786 force-pushed the fix/forwardauth-wildcard-injection branch from 2267e00 to 7582a00 Compare May 13, 2026 14:52
@awais786 awais786 merged commit 0c44e25 into foss-main May 13, 2026
8 of 10 checks passed
awais786 added a commit that referenced this pull request May 16, 2026
The test fed `%@%.%` to assert that SQL wildcard characters in the
proxy email header don't match existing users via Op.iLike. But that
input isn't a syntactically valid email — Sequelize's isEmail
validator on the User model rejects it before User.create runs, so
the test crashes with SequelizeValidationError on the
"provision new user" path rather than reaching its assertions.

Switch to `attacker%_@evil.example.com` — RFC-5321 allows `%` and `_`
in the local part so isEmail accepts it, and the `%` is still a SQL
LIKE wildcard that would match other users under Op.iLike. The
security property under test (exact-match lookup, not Op.iLike) is
preserved; the test now actually exercises both the lookup miss AND
the new-user provisioning that follows.

Also adds a positive assertion that the provisioned user's email is
the exact wildcard string — verifying we didn't resolve to a
pattern-matched existing user.

This was a pre-existing flake on test-server shard 3 that was masked
by parallel-shard cancellation; the test never actually passed since
PR #18 added it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant