Skip to content

fix(semver): remove exponential backtracking from the prerelease pattern - #3075

Merged
lidge-jun merged 1 commit into
devfrom
codex/semver-redos
Aug 31, 2026
Merged

fix(semver): remove exponential backtracking from the prerelease pattern#3075
lidge-jun merged 1 commit into
devfrom
codex/semver-redos

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

CodeQL flagged js/redos at high severity on the v2.38.0 promotion PR (#3073). It is a genuine finding, not noise, and the release is held until it lands.

src/lib/strict-semver.ts matched the prerelease section with the semver.org pattern verbatim. Its three identifier alternatives overlap:

0 | [1-9]\d* | [0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*

Wrapping that in (?:\.identifier)* gives a backtracking engine an exponential number of ways to split the same string.

The cost is measured, not theoretical

The attack input CodeQL named — 0.0.0-0. followed by repetitions of --. — measured on this machine:

input length repetitions time
68 20 16.6ms
98 30 522.4ms
125 39 524.2ms

522ms for a single 125-character input. That is inside the 128-character ceiling this module already enforced, and inside the 96-character ceiling its only caller (validatedVersion in src/codex/cli-install-provenance.ts) passes.

That is the part worth being precise about: a length cap does not fix superlinear growth. It only decides where on the curve the input lands. The guard was doing its job and the blowup happened anyway.

The fix

Match the prerelease in one non-backtracking pass ([0-9A-Za-z.-]+), then validate each identifier separately with anchored regexes that contain no repetition of an alternation. Each check is linear in that identifier's length.

Same 125-character input afterwards: 0.024ms.

Behaviour is unchanged

36 cases covering the semver.org examples plus the rejections — leading zeroes, empty identifiers, trailing and doubled dots, numeric identifiers with leading zeroes, non-semver shapes — agree with the old pattern exactly, 0 mismatches. The parsed core and prerelease shape is identical, including the bigint versus string split (1.0.0-0.3.7-x still yields [0n, 3n, "7-x"]).

Verification

Regressions driven red against the old pattern:

Expected: < 50
Received: 524.3668749999999
(fail) parseStrictSemver ReDoS resistance > the flagged attack shape stays linear at the length ceiling

Expected: < 50
Received: 520.2574579999999
(fail) parseStrictSemver ReDoS resistance > cost does not grow with the number of repetitions

With the fix: bun test tests/strict-semver.test.ts tests/codex-cli-install-provenance.test.ts23 pass / 0 fail. bun run typecheck and bun run privacy:scan clean.

The other alert

CodeQL also raised js/stack-trace-exposure at medium on src/server/auth-cors.ts:235 (jsonResponse). I checked it rather than assuming: there is no .stack access anywhere in the server response paths, and every jsonResponse error caller passes error.message only. It is a false positive from the taint tracker following JSON.stringify(data), so nothing is changed for it.

Checklist

  • Regression tests added, driven red-first with the measured times recorded
  • Behavioural equivalence verified against the old pattern across 36 cases
  • bun run typecheck passes
  • bun run privacy:scan passes
  • Targets dev

Summary by CodeRabbit

  • Bug Fixes

    • Improved semantic version parsing for prerelease identifiers.
    • Correctly rejects invalid versions, including malformed identifiers and leading zeroes.
    • Prevented slow parsing for specially crafted or overly long version strings.
    • Preserved support for numeric and alphanumeric prerelease components.
  • Tests

    • Added comprehensive coverage for valid SemVer examples, invalid formats, length limits, and performance under stress.

CodeQL js/redos, high severity, found on the v2.38.0 promotion PR.

The prerelease section used the semver.org pattern verbatim. Its three identifier
alternatives overlap:

  0 | [1-9]\d* | [0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*

and wrapping that in (?:\.identifier)* lets a backtracking engine try an
exponential number of ways to split the same string.

The cost is real, not theoretical. "0.0.0-0." followed by repetitions of "--."
took 522ms for a single 125-character input — inside the 128-character ceiling
this module enforces, and inside the 96-character one its only caller passes. A
length cap does not fix superlinear growth; it only picks where on the curve the
input lands. Going from 20 to 39 repetitions moved 16ms to 524ms.

Match the prerelease in one non-backtracking pass and validate each identifier
separately with anchored regexes that contain no repeated alternation. Same
input took 0.024ms afterwards.

Behaviour is unchanged: 36 grammar cases spanning the semver.org examples and
the leading-zero, empty-identifier and malformed rejections agree with the old
pattern exactly, and the parsed core/prerelease shape is identical.

The regressions fail against the old pattern at 524ms and 520ms.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 31, 2026 10:35
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T10:39:16.225302Z 2858fe7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the bug Something isn't working label Aug 31, 2026
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ffe468cd-ef78-4d2f-95ce-2b169a4c6fa9

📥 Commits

Reviewing files that changed from the base of the PR and between 7ee96b9 and 2858fe7.

📒 Files selected for processing (2)
  • src/lib/strict-semver.ts
  • tests/strict-semver.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The semver parser replaces a backtracking-prone prerelease regex with loose matching and per-identifier validation. Tests verify ReDoS resistance, length limits, semver grammar, prerelease splitting, numeric conversion, and empty prerelease results.

Changes

Strict semver hardening

Layer / File(s) Summary
Prerelease matching and validation
src/lib/strict-semver.ts:1-28, src/lib/strict-semver.ts:40-45
The parser captures prerelease text with a non-backtracking character group, validates each dot-separated identifier, rejects invalid numeric forms, and preserves BigInt conversion for numeric identifiers.
Parser regression coverage
tests/strict-semver.test.ts:1-107
Tests cover attack-shaped inputs, length limits, valid semver examples, invalid identifiers, prerelease splitting, and versions without prerelease identifiers.

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

Merge Risk: ⚪ Minimal · up to 2858f

This PR replaces a backtracking-prone prerelease parser with linear validation while preserving existing semver behavior. The change is localized, and no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: removing exponential backtracking from the semver prerelease pattern in src/lib/strict-semver.ts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/semver-redos

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2858fe7015

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +19 to +21
const started = performance.now();
expect(parseStrictSemver(attack)).toBeNull();
const elapsed = performance.now() - started;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Measure parser work with CPU time

On a loaded CI worker, the process can be descheduled or paused for GC between these performance.now() calls, so a correct linear parse taking microseconds of CPU can still report more than 50 ms and fail the suite; the measure() helper below has the same wall-clock dependency. Use process.cpuUsage()—as the repository already does for bounded-work regression tests—or another deterministic mechanism that excludes scheduler pauses.

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 67 / 80

이 PR은 새 기능이 아닙니다. 지금 dev HEAD 7ee96b94e(#3058)에 그대로 있는 버전 문자열 정규식에서, 미리보기(prerelease) 부분이 같은 글자를 너무 많은 방법으로 나눠 보다가 시간이 폭발하는 구멍을 막는 고침입니다. 로컬에서 origin/dev를 다시 받아 본 SHA는 스냅샷과 같습니다. src/lib/strict-semver.ts 1줄의 STRICT_SEMVER_RE는 아직 예전 모양입니다. 미리보기 식별자 하나가 0 이거나, 앞에 0이 없는 숫자이거나, 글자·빼기가 하나라도 들어 있는 글자숫자 중 하나인데, 이 세 갈래가 같은 글자를 겹쳐서 먹을 수 있습니다. 그 덩어리를 점(.)으로 여러 번 반복하니, 엔진은 점을 어디에 둘지 경우의 수를 지수로 셉니다. CodeQL이 v2.38.0 승격 PR(#3073)에서 js/redos를 높은 수위로 잡았고, 본문이 말한 대로 승격은 이 고침이 올라가기 전에는 나가지 못합니다.

구멍이 진짜인지는 패턴만 봐도 알 수 있습니다. 식별자 세 갈래가 겹치고, 그 식별자를 (?:\.ident)*로 감싼 모양은 교과서에 나오는 백트래킹 폭발입니다. 공격 입력은 CodeQL이 집어 준 0.0.0-0. 뒤에 --.를 반복한 것입니다. 본문 표는 68글자(20번)에서 16.6ms, 98글자(30번)에서 522ms, 125글자(39번)에서 524ms입니다. 이 모듈의 길이 천장 128글자 안이고, 유일한 호출자 validatedVersion이 넘기는 천장 96글자보다 조금 긴 지점입니다. 중요한 점은 이겁니다. 길이 제한은 초선형 증가를 고치지 못합니다. 곡선 어디서 자를지만 정합니다. 96글자면 표의 68과 98 사이라서, 호출자가 이미 자르고 있어도 수백 밀리초가 날 수 있습니다. 기본값 128을 쓰는 다음 호출자가 생기면 125글자 경로(약 0.5초)가 바로 열립니다.

이 함수를 지금 부르는 곳은 src/codex/cli-install-provenance.ts뿐입니다. 126~128줄 validatedVersionparseStrictSemver(value, 96)을 씁니다. 199줄은 디스크에 저장된 Codex 런타임 상태의 selectedVersion이고, 508줄은 로컬 @openai/codex package.jsonversion입니다. HTTP 요청 본문을 그대로 넣는 길이 아닙니다. doctor·status·설치 출처를 읽을 때 로컬 파일을 한 번 보는 길입니다. 그래서 점수를 75까지 올리지 않습니다. 다만 그 파일은 우리 패키지가 아닙니다. 남이 심어 둔 Codex 설치나 오염된 런타임 상태면 96글자 공격 문자열이 들어올 수 있습니다. 네트워크 핫패스는 아니지만, 신뢰하는 우리 버전 숫자만 본다고 말하기도 어렵습니다.

고치는 방법은 맞습니다. 미리보기 덩어리는 [0-9A-Za-z.-]+로 한 번에 집어 오고(점이든 글자든 되돌아가지 않음), 점을 기준으로 나눈 뒤 식별자마다 고정된 정규식으로 검사합니다. 숫자는 0 또는 앞에 0이 없는 숫자, 글자숫자는 빼기·글자가 하나라도 있어야 하고 숫자만으로 된 것은 거절합니다. 빈 조각은 거절하니 끝 점과 점이 두 번 연속인 것도 걸립니다. 본문이 예전에 524ms 나오던 같은 125글자 입력이 고친 뒤에는 0.024ms라고 했고, 문법 36개가 예전 패턴과 0건 어긋난다고 했습니다. 새로 생긴 tests/strict-semver.test.ts는 그 공격을 50ms 안에 끝나게 잠그고, semver.org 예와 거절 목록, 1.0.0-0.3.7-x[0n, 3n, "7-x"]로 나뉘는 것까지 잠급니다. types.ts/config.ts 분할과 무관하고, 같은 주제로 닫아야 할 다른 PR도 없습니다. 프리뷰 배포는 계획에 없습니다.

점수 67은 '진짜 ReDoS이고 고침이 작고 맞지만, 요청마다 도는 파서가 아니라서 75는 아니다'라는 뜻입니다. #3073 승격을 붙잡고 있는 CodeQL 높은 수위 경고를 푸는 값입니다. 제품 동작을 바꾸려는 PR이 아닙니다. 예전과 같은 수락·거절을 유지한 채, 엔진이 같은 문자열을 지수로 나눠 보지 않게만 바꿉니다.

src/lib/strict-semver.ts 1줄 (현재 dev) - 미리보기 식별자 세 갈래가 겹치고 그걸 점 반복으로 감싼 예전 패턴이 아직 HEAD에 있다. 이 PR이 없애려는 바로 그 줄이다
tests/strict-semver.test.ts 라인 25 - 50ms 벽시계 상한은 고친 뒤 0.024ms라서 여유는 크다. 다만 한 번 재서 끝나는 측정이라 러너가 아주 바쁠 때 오탐이 날 수는 있다. 블로커는 아니다
tests/strict-semver.test.ts 라인 28-42 - 테스트 이름은 반복 횟수에 따라 비용이 안 커진다고 하지만, 단언은 둘 다 50ms 미만일 뿐이다. 선형성을 숫자로 비교하지는 않는다. 예전이 다시 들어오면 39회가 500ms를 넘겨 어차피 빨개지니, 구멍 자체는 잠긴다
src/lib/strict-semver.ts 라인 15 (이 PR) - 빌드 메타데이터는 예전처럼 [0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*를 그대로 둔다. 식별자 대안이 겹치지 않아서 지수 폭발은 아니다. CodeQL이 미리보기만 잡은 이유와 맞다. 이번에 손대지 않은 게 맞다
src/codex/cli-install-provenance.ts 라인 128 - 유일한 호출자다. 천장 96, 입력은 로컬 런타임 상태와 package.json version이다. HTTP 본문이 아니다. 그래서 점수를 75로 올리지 않는다

메인테이너의 판단이 필요한 지점

  • CI가 초록이면 지금 머지해서 [WRONG BRANCH] promote dev onto main for v2.38.0 #3073 CodeQL을 풀지, 승격 PR이 다시 스캔될 때까지 기다릴지. 지금 합치는 편이 맞다
  • 빌드 메타데이터 패턴까지 이 PR에서 같은 방식으로 나눌지. 나누지 않는 편이 맞다. 지수가 아니고 범위가 커진다
  • 50ms 타이밍 단언을 더 느슨하게 둘지. 지금은 0.024ms 대비 여유가 커서 그대로 둬도 된다
  • 라벨은 바꾸지 않는다. bug 하나만 붙어 있고 맞다

너의 추천
CI가 초록이면 머지하세요. 현재 dev 1줄의 겹치는 미리보기 패턴이 아직 살아 있고, 길이 천장 안에서도 초선형으로 느려집니다. 고침은 한 번 훑고 식별자만 검사하는 쪽이라 문법이 예전과 같고, 승격 PR(#3073)의 CodeQL 높은 수위 경고를 푸는 값입니다. 빌드 메타데이터는 건드리지 마세요. types.ts/config.ts 분할로 닫을 대상이 아니고, 중복 PR도 없습니다. 프리뷰 배포는 계획에 없습니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun merged commit f43f6fb into dev Aug 31, 2026
27 checks passed
@lidge-jun
lidge-jun deleted the codex/semver-redos branch August 31, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant