Skip to content

fix(oauth2): honour RedirectURI matching_mode in CORS origin check [Buzzvil fork patch]#2

Merged
dlddu merged 2 commits into
base/2026.2.1from
buzzvil/cors-regex-redirect-2026.2.1
Jul 20, 2026
Merged

fix(oauth2): honour RedirectURI matching_mode in CORS origin check [Buzzvil fork patch]#2
dlddu merged 2 commits into
base/2026.2.1from
buzzvil/cors-regex-redirect-2026.2.1

Conversation

@dlddu

@dlddu dlddu commented Jul 17, 2026

Copy link
Copy Markdown

문제

Authentik OAuth2 provider의 redirect URI 항목은 matching_modeSTRICTREGEX를 지원합니다. views/authorize.pyredirect_uri 쿼리 파라미터 검증 시 두 모드를 모두 올바르게 처리하지만, CORS origin 검사(authentik/providers/oauth2/utils.py::cors_allow)는 matching_mode완전히 무시했습니다.

  • 호출부(token.py, userinfo.py, provider.py)가 [x.url for x in redirect_uris]로 URL 문자열만 뽑아 넘김 → matching_mode 유실
  • cors_allow는 각 항목을 urlparse해서 scheme/host/port 동등 비교만 수행

증상: REGEX 모드 redirect URI로 설정된 브라우저 앱(SPA)은 /authorize/는 통과하지만, /application/o/token/(및 /userinfo/, provider-info 엔드포인트)로 보내는 fetch()가 브라우저에서 차단됩니다. 응답에 Access-Control-Allow-Origin 헤더가 없기 때문입니다. 워커 로그에는 설정한 정규식이 allowed에 그대로 찍힌 채 CORS: Origin is not an allowed origin이 남고 매칭은 절대 되지 않습니다.

변경

  • cors_allowRedirectURI | str을 받아 matching_mode로 분기
    • STRICT: 기존 scheme+host+port 비교 유지 (회귀 안전)
    • REGEX: 요청 Originre.fullmatch로 매칭 — views/authorize.py와 동일 시맨틱
  • 잘못된 정규식은 re.error를 잡아 로깅 후 skip → 항목 하나가 요청을 500으로 떨구지 않음
  • 3개 호출부가 URL 문자열로 평탄화하지 않고 RedirectURI 객체를 그대로 전달
  • 문자열 항목은 하위호환을 위해 계속 허용(STRICT로 취급)
  • 모델/스키마 변경 없음 — matching_mode는 이미 각 항목에 저장됨

Origin vs. path (운영 주의)

CORS Origin은 scheme+host[:port]로 경로가 없습니다. 따라서 경로가 필수인 full redirect URI용 정규식은 bare Origin과 매칭되지 않습니다. REGEX CORS를 쓰려면 다음 중 하나로 항목을 작성하세요:

  1. 경로 그룹을 선택적으로 (권장 — 한 항목으로 authorize + CORS 모두 커버):
    https://app-\w+\.example\.com(/callback)?
    
  2. 또는 origin 전용 REGEX 항목을 별도로 추가:
    https://app-\w+\.example\.com/callback   # /authorize redirect_uri 용
    https://app-\w+\.example\.com            # CORS Origin 용
    

테스트

authentik/providers/oauth2/tests/test_utils.py 신규 추가 — strict 회귀, regex 매칭/미매칭, 선택적 경로 그룹, 필수 경로 미매칭(문서화된 동작), 잘못된 정규식 skip/500 방지, OPTIONS preflight, bare-string 하위호환, strict+regex 혼합 구성.

로컬 검증: 매칭 로직 13개 시나리오 standalone 재현 테스트 전부 통과. (Django TestCase 전체 실행은 Postgres/Redis 필요해 이 환경에서는 미실행)

출처 · 배포

🤖 Generated with Claude Code

…uzzvil fork patch]

The OAuth2 provider supports STRICT and REGEX matching_mode for redirect
URI entries. views/authorize.py honours both when validating the
redirect_uri query param, but cors_allow() ignored matching_mode
entirely: callers flattened redirect_uris to bare url strings and
cors_allow only did scheme/host/port equality via urlparse.

Effect: a browser app configured with a REGEX redirect URI completes
/authorize/ but its fetch() to /application/o/token/ (and /userinfo/,
and the provider-info endpoint) is blocked because the response carries
no Access-Control-Allow-Origin header. Worker logs show
"CORS: Origin is not an allowed origin" with the regex verbatim in
`allowed`, never matched.

- cors_allow now accepts RedirectURI | str and branches on
  matching_mode. Strict entries keep the existing scheme+host+port
  comparison (regression-safe). Regex entries are matched against the
  request Origin via re.fullmatch, mirroring views/authorize.py.
- Malformed regexes raise re.error, which is caught, logged, and
  skipped so one bad entry cannot 500 the request.
- The three call sites (token.py, userinfo.py, provider.py) pass
  RedirectURI objects through instead of flattening to url strings.
- Bare string entries are still accepted (treated as STRICT) for
  backwards compatibility.

A CORS Origin is scheme+host[:port] with no path, so a regex written
for the full redirect URI with a mandatory path will not match a bare
Origin; write it as host(/path)? or add a separate origin-only entry.

Backport of upstream goauthentik#22179 (closes goauthentik#3084) onto
the Buzzvil 2026.2.1 fork.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

OAuth2 CORS 처리가 문자열과 RedirectURI를 모두 받아 STRICT 또는 REGEX 방식으로 Origin을 매칭하도록 변경되었습니다. 관련 OAuth2 뷰는 원본 redirect_uris를 전달하며, 신규 테스트가 매칭·오류 정규식·OPTIONS 요청을 검증합니다. Dockerfile과 릴리스 워크플로에는 OAuth2 패치 파일 및 sparse-checkout 경로가 추가되었습니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 OAuth2 CORS에서 RedirectURI matching_mode를 반영하는 핵심 변경을 정확히 요약합니다.
Description check ✅ Passed 문제, 변경, 테스트, 관련 이슈를 포함해 핵심 내용이 충분히 설명되어 있습니다.
Linked Issues check ✅ Passed REGEX/STRICT 분기, invalid regex skip, 호출부 전달 변경, 테스트 추가로 #3084 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed Dockerfile·워크플로우·테스트 추가는 OAuth2 CORS 패치 적용과 검증에 직접 관련됩니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch buzzvil/cors-regex-redirect-2026.2.1

Comment @coderabbitai help to get the list of available commands.

…ed image

The patched-image workflow builds an overlay on ghcr.io/goauthentik/server
by COPYing only the changed Python files (sparse-checkout + Dockerfile COPY).
Until now it carried the two SAML RelayState files only, so a `-relaystate`
tag build would NOT include the oauth2 CORS regex fix.

- .buzzvil/Dockerfile: COPY the four oauth2 runtime files
  (utils.py + views/token.py, userinfo.py, provider.py). All four must ship
  together — the views pass RedirectURI objects into cors_allow's patched
  signature. The test file is not runtime code and is deliberately excluded.
- workflow sparse-checkout: add the same four files so they exist in the
  build context.
- generalise the SAML-only comments and release title now that the image
  bundles both fork patches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dlddu
dlddu marked this pull request as ready for review July 20, 2026 02:18
@dlddu
dlddu requested review from a team, C0deWave and hugolee-woobuntu and removed request for a team July 20, 2026 02:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
authentik/providers/oauth2/utils.py (1)

81-91: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

REGEX redirect URI의 실행 비용을 제한하세요. RedirectURIMatchingMode.REGEX는 저장 시 문법만 검사하고, 요청마다 Originre.fullmatch()를 그대로 수행합니다. 백트래킹이 큰 패턴은 워커를 오래 점유할 수 있으니, 저장 단계에서 허용 문법/복잡도 제한을 두거나 안전한 정규식 엔진으로 바꾸세요. 잘못된 패턴은 지금처럼 로그 후 건너뛰면 됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authentik/providers/oauth2/utils.py` around lines 81 - 91, Limit regex
redirect URI execution cost by enforcing allowed syntax and complexity when
REGEX entries are stored, or replace Python re.fullmatch in the REGEX matching
path with a safe linear-time regex engine. Preserve the existing behavior for
invalid patterns: log the parsing failure and skip the entry.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-patched-ecr.yaml:
- Around line 78-79: Update the workflow step containing the gh release --title
and --notes arguments to pass github.ref_name through the step’s env
configuration, alongside the existing Line 76 usage. Replace every direct ${{
github.ref_name }} interpolation in the shell command with a quoted shell
variable expansion, preserving the release title and notes content.

---

Nitpick comments:
In `@authentik/providers/oauth2/utils.py`:
- Around line 81-91: Limit regex redirect URI execution cost by enforcing
allowed syntax and complexity when REGEX entries are stored, or replace Python
re.fullmatch in the REGEX matching path with a safe linear-time regex engine.
Preserve the existing behavior for invalid patterns: log the parsing failure and
skip the entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e392d749-67e5-4973-9b88-59918b6cce61

📥 Commits

Reviewing files that changed from the base of the PR and between f054a81 and 15df72a.

📒 Files selected for processing (7)
  • .buzzvil/Dockerfile
  • .github/workflows/build-patched-ecr.yaml
  • authentik/providers/oauth2/tests/test_utils.py
  • authentik/providers/oauth2/utils.py
  • authentik/providers/oauth2/views/provider.py
  • authentik/providers/oauth2/views/token.py
  • authentik/providers/oauth2/views/userinfo.py

Comment on lines +78 to +79
--title "${{ github.ref_name }} — Buzzvil patched authentik (SAML RelayState + OAuth2 CORS regex)" \
--notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${{ github.ref_name }}\` (multi-arch amd64/arm64, public). 오버레이 패치 목록·빌드·제거 조건은 .buzzvil/Dockerfile 및 저장소 README 참고." \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

태그명을 셸 명령에 직접 보간하지 마세요.

Line 78-79의 ${{ github.ref_name }}는 셸 실행 전에 확장됩니다. $() 등을 포함한 태그가 생성 가능하면 명령 치환으로 실행될 수 있습니다. 기존 Line 76도 함께 env 변수로 옮긴 뒤 인용된 변수 확장만 사용하세요.

수정 예시
         env:
           GH_TOKEN: ${{ github.token }}
+          RELEASE_TAG: ${{ github.ref_name }}
         run: |
-          gh release create "${{ github.ref_name }}" \
+          gh release create "$RELEASE_TAG" \
             --repo "${{ github.repository }}" \
-            --title "${{ github.ref_name }} — Buzzvil patched authentik (SAML RelayState + OAuth2 CORS regex)" \
-            --notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${{ github.ref_name }}\` (multi-arch amd64/arm64, public). 오버레이 패치 목록·빌드·제거 조건은 .buzzvil/Dockerfile 및 저장소 README 참고." \
+            --title "$RELEASE_TAG — Buzzvil patched authentik (SAML RelayState + OAuth2 CORS regex)" \
+            --notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${RELEASE_TAG}\` (multi-arch amd64/arm64, public). 오버레이 패치 목록·빌드·제거 조건은 .buzzvil/Dockerfile 및 저장소 README 참고." \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--title "${{ github.ref_name }} — Buzzvil patched authentik (SAML RelayState + OAuth2 CORS regex)" \
--notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${{ github.ref_name }}\` (multi-arch amd64/arm64, public). 오버레이 패치 목록·빌드·제거 조건은 .buzzvil/Dockerfile 및 저장소 README 참고." \
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
gh release create "$RELEASE_TAG" \
--repo "${{ github.repository }}" \
- --title "${{ github.ref_name }} — Buzzvil patched authentik (SAML RelayState + OAuth2 CORS regex)" \
--title "$RELEASE_TAG — Buzzvil patched authentik (SAML RelayState + OAuth2 CORS regex)" \
--notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${RELEASE_TAG}\` (multi-arch amd64/arm64, public). 오버레이 패치 목록·빌드·제거 조건은 .buzzvil/Dockerfile 및 저장소 README 참고." \
🧰 Tools
🪛 zizmor (1.26.1)

[error] 78-78: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 79-79: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-patched-ecr.yaml around lines 78 - 79, Update the
workflow step containing the gh release --title and --notes arguments to pass
github.ref_name through the step’s env configuration, alongside the existing
Line 76 usage. Replace every direct ${{ github.ref_name }} interpolation in the
shell command with a quoted shell variable expansion, preserving the release
title and notes content.

Source: Linters/SAST tools

@C0deWave

Copy link
Copy Markdown

요약

cors_allow가 redirect URI의 matching_mode를 무시하고 항상 scheme/host/port 동등 비교만 하던 문제를 고쳐, REGEX 모드 항목을 요청 Origin에 대해 re.fullmatch로 매칭하도록 변경했습니다. 3개 호출부(token.py/userinfo.py/provider.py)가 URL 문자열로 평탄화하는 대신 RedirectURI 객체를 그대로 넘기고, cors_allowRedirectURI | str을 받아 문자열은 STRICT로 하위호환 처리합니다. 로직은 views/authorize.py::check_redirect_uri의 시맨틱을 충실히 미러링하며, STRICT 경로·OPTIONS preflight·잘못된 정규식 skip까지 테스트로 잘 커버됩니다.

발견된 이슈

전반적으로 코드는 견고하며 동작에 영향을 주는 버그(🔴/🟡)는 발견하지 못했습니다. 아래는 확인·검증한 사항과 사소한 개선 제안입니다.

  • 🔵 제안 — 테스트가 SimpleTestCase가 아닌 TestCase 사용 (authentik/providers/oauth2/tests/test_utils.py:13)
    신규 테스트들은 RequestFactoryRedirectURI를 직접 생성해 검증할 뿐 DB에 전혀 접근하지 않습니다. 그런데 django.test.TestCase는 테스트 DB(Postgres) 셋업을 요구합니다 — PR 설명에서 "Postgres/Redis 필요해 이 환경에서는 미실행"이라고 한 원인이 바로 이것입니다. SimpleTestCase로 바꾸면 DB 없이 CI/로컬에서 그대로 실행되어 실제로 검증됩니다. (동작 버그는 아니지만, 검증 가능성이 올라가는 실질적 개선입니다.)

  • 🔵 확인 완료 — STRICT 경로 회귀 안전
    원본은 break 없이 전체 항목을 순회했고, 변경본은 매칭 시 break 합니다(utils.py:78-79). allowed는 한번 True가 되면 유지되므로 최종 결과는 동일하며, OPTIONS(allowed=True 초기화, utils.py:63)도 영향 없습니다. 기존 test_token.py의 STRICT 기반 CORS 단언들도 그대로 유효합니다.

  • 🔵 운영 주의 — 기존 REGEX provider는 자동으로 CORS가 열리지 않음 (PR에 이미 문서화됨)
    Origin에는 경로가 없어, 경로가 필수인 full-redirect-URI용 정규식(예: .../callback)은 bare Origin과 매칭되지 않습니다(utils.py:82). 즉 이미 REGEX로 운영 중인 앱은 이 패치만으로 CORS가 바로 뚫리지 않고, host(/path)? 형태로 정규식을 고치거나 origin 전용 항목을 추가해야 합니다. PR 본문의 "Origin vs. path" 절과 테스트(test_regex_full_redirect_uri_does_not_match_bare_origin)가 이 동작을 명확히 설명하고 있어 문제는 아니지만, 배포 시 안내가 필요한 지점입니다.

의심 코드

  • 정규식 ReDoS 표면 확대utils.py:82 re.fullmatch(entry.url, origin)
    관리자 설정 정규식(신뢰 입력)을 공격자 제어 Origin 헤더(비신뢰 입력)에 대해 실행합니다. 파국적 백트래킹(catastrophic backtracking)이 가능한 정규식이 설정돼 있으면, 긴 Origin으로 워커 스레드를 점유시키는 ReDoS 가능성이 이론상 존재합니다.

    • 다만 이 PR이 새로 만든 취약점은 아닙니다. 동일 정규식이 이미 views/authorize.py:215에서 공격자 제어 redirect_uri(비인증 /authorize)에 대해 실행되고 있어, 근본 노출은 기존과 같고 오히려 authorize 쪽이 더 큰 표면입니다.
    • 확인 필요 사항: provider.pyProviderInfoView는 인증이 없는 공개 엔드포인트(.well-known provider-info)입니다. 이 패치로 인해 해당 비인증 경로에서도 REGEX 매칭이 돌게 됩니다(application slug만 알면 도달). token.py/userinfo.py는 각각 provider/token 해석(사실상 인증)을 거친 뒤에만 정규식이 돌아 노출이 제한적이지만, provider-info는 그렇지 않습니다. 실제 위험도는 낮고(정규식은 관리자 신뢰 입력) 기존 authorize 노출과 동질이지만, 정규식 복잡도/타임아웃 가드가 없다는 점은 인지하고 넘어가는 것이 좋습니다. (가드를 넣는다면 이 백포트 범위를 넘는 upstream-wide 변경이므로, 이 PR에서 굳이 다룰 필요는 없다고 봅니다.)
  • OPTIONS preflight의 무조건 Origin 반사utils.py:63
    기존과 동일하게, OPTIONS는 매칭 없이 임의 Origin을 반사합니다. 다만 실제(GET/POST) 요청은 다시 cors_allow를 거쳐 반드시 매칭돼야 Access-Control-Allow-Origin이 붙으므로, preflight가 관대해도 실제 응답에서 검증이 강제됩니다. 이 PR이 바꾼 부분이 아니고 설계상 안전하여 문제 없음을 확인했습니다.

종합 평가

APPROVE

views/authorize.py의 검증 로직을 정확히 미러링한 타당한 백포트입니다. fail-closed(미매칭·잘못된 정규식 시 헤더 미부여, 500 없음), 문자열 하위호환, STRICT 회귀 안전이 모두 지켜졌고 테스트가 핵심 시나리오(strict/regex 매칭·미매칭, 선택적 경로 그룹, 잘못된 정규식, OPTIONS, 혼합 구성)를 잘 덮습니다. Dockerfile 오버레이도 시그니처가 바뀐 utils.py와 3개 호출부를 함께 COPY하도록 되어 있어 부분 적용으로 인한 urlparse(RedirectURI) 크래시 위험이 없습니다. 위 🔵 제안(특히 SimpleTestCase 전환)은 머지 차단 사유가 아닌 개선 항목입니다.

@C0deWave C0deWave left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이정도면 오픈소스에 기여 해도 되겠는데요???
LGTM

@dlddu
dlddu merged commit 2d589b1 into base/2026.2.1 Jul 20, 2026
3 checks passed
@dlddu
dlddu deleted the buzzvil/cors-regex-redirect-2026.2.1 branch July 20, 2026 02:37
@dlddu

dlddu commented Jul 20, 2026

Copy link
Copy Markdown
Author

@C0deWave 이거 ai가 업스트림 레포에 pr 올라와있던 거 베껴왔다고 했습니다

@C0deWave

Copy link
Copy Markdown

아하 제가 이슈만 봤군요

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