Skip to content

πŸ›‘οΈ Sentinel: [CRITICAL/HIGH] Fix 이메일 CRLF μΈμ μ…˜ 및 이쀑 ν™•μž₯자 우회 취약점 - #1164

Closed
seonghobae wants to merge 2 commits into
developfrom
sentinel-fix-crlf-and-double-extension-8488562200955951224
Closed

πŸ›‘οΈ Sentinel: [CRITICAL/HIGH] Fix 이메일 CRLF μΈμ μ…˜ 및 이쀑 ν™•μž₯자 우회 취약점#1164
seonghobae wants to merge 2 commits into
developfrom
sentinel-fix-crlf-and-double-extension-8488562200955951224

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🚨 Severity: HIGH
πŸ’‘ Vulnerability:

  1. Pydantic μ •κ·œμ‹μ˜ ν•œκ³„λ‘œ 인해 이메일 헀더(to, subject λ“±)에 CRLF(\r\n) μ£Όμž…μ΄ κ°€λŠ₯ν•˜μ—¬ μž„μ˜μ˜ SMTP λͺ…λ Ήμ–΄(예: MAIL FROM)κ°€ 싀행될 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€ (CRLF Injection).
  2. 파일 μ—…λ‘œλ“œ 검증이 λ‹¨μˆœ .endswith()에 μ˜μ‘΄ν•˜μ—¬ 쀑간에 μ•…μ„± ν™•μž₯μžκ°€ ν¬ν•¨λœ 이쀑 ν™•μž₯자 파일(예: malware.exe.eml)을 ν†΅κ³Όμ‹œν‚¬ 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
    🎯 Impact: κ³΅κ²©μžκ°€ κΆŒν•œ μ—†λŠ” 이메일 전솑(슀팸 λ“±)을 μˆ˜ν–‰ν•˜κ±°λ‚˜, μ‹œμŠ€ν…œμ— μ•…μ„± 슀크립트/μ‹€ν–‰ νŒŒμΌμ„ μ—…λ‘œλ“œν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
    πŸ”§ Fix:
  • backend/api/emails.py의 SendEmailRequest λͺ¨λΈμ— @field_validator(mode="before")λ₯Ό μΆ”κ°€ν•˜μ—¬ 이메일 헀더 κ΄€λ ¨ ν•„λ“œμ— μƒˆμ€„ 문자(CRLF)κ°€ ν¬ν•¨λœ 경우 λͺ…μ‹œμ μœΌλ‘œ κ±°λΆ€ν•˜λ„λ‘ μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.
  • 파일 μ—…λ‘œλ“œ 둜직(import_email_files)μ—μ„œ 파일 이름을 .으둜 λΆ„ν• ν•˜μ—¬ λͺ¨λ“  μ„Έκ·Έλ¨ΌνŠΈλ₯Ό κ²€μ‚¬ν•˜κ³  μ•…μ„± ν™•μž₯자(예: .exe, .sh)κ°€ ν¬ν•¨λœ 경우 μ—…λ‘œλ“œλ₯Ό μ°¨λ‹¨ν•˜λ„λ‘ μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.
    βœ… Verification: cd backend && uv run pytest tests/test_emails_api.pyλ₯Ό μ‹€ν–‰ν•˜μ—¬ ν•΄λ‹Ή μ—”λ“œν¬μΈνŠΈμ˜ ν…ŒμŠ€νŠΈκ°€ λͺ¨λ‘ 톡과됨을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.

PR created automatically by Jules for task 8488562200955951224 started by @seonghobae

Summary by CodeRabbit

  • Security
    • Strengthened email validation to block CRLF characters in header fields, helping prevent header injection.
    • Improved uploaded-file checks to reject dangerous double-extension filenames while allowing supported formats.
  • Maintenance
    • Updated frontend framework, image-processing, and CSS tooling packages.

이 컀밋은 backend/api/emails.py λͺ¨λ“ˆμ˜ 취약점을 μˆ˜μ •ν•©λ‹ˆλ‹€.
1. `field_validator`λ₯Ό μΆ”κ°€ν•˜μ—¬ 이메일 헀더 ν•„λ“œμ˜ μƒˆμ€„ 문자λ₯Ό μ°¨λ‹¨ν•˜μ—¬ CRLF μΈμ μ…˜μ„ λ°©μ§€ν•©λ‹ˆλ‹€.
2. `import_email_files`의 파일 μ—…λ‘œλ“œ 검증을 κ°œμ„ ν•˜μ—¬ 파일 이름을 λΆ„ν• ν•˜κ³  이쀑 ν™•μž₯자 우회λ₯Ό νƒμ§€ν•©λ‹ˆλ‹€.
Copilot AI review requested due to automatic review settings July 27, 2026 14:27
@google-labs-jules

Copy link
Copy Markdown
Contributor

πŸ‘‹ Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a πŸ‘€ emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

πŸ“ Walkthrough

Walkthrough

The PR strengthens email header and imported filename validation against CRLF injection and double-extension bypasses, documents the security fixes, and updates selected frontend dependency versions.

Changes

Email and upload security validation

Layer / File(s) Summary
Upload filename validation
backend/api/emails.py
/import-files now requires a filename, validates the final extension, and rejects dangerous extensions in earlier filename segments.
Email header validation
backend/api/emails.py, .jules/sentinel.md
SendEmailRequest rejects CR/LF characters in selected header fields, and the sentinel documents both security protections.

Frontend dependency updates

Layer / File(s) Summary
Frontend dependency version updates
frontend/package.json
Next, sharp, and postcss versions were updated while existing postcss override and resolution pins remain unchanged.

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

Possibly related PRs

Suggested reviewers: copilot

πŸš₯ Pre-merge checks | βœ… 4
βœ… 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 summarizes the main security fix: CRLF injection and double-extension bypass prevention.
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.
✨ 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 sentinel-fix-crlf-and-double-extension-8488562200955951224

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

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

PR governance metadata gate is not ready for 0615a8f30c59cb7d4a36b5a07f6455ace96444fb:

  • Review decision is CHANGES_REQUESTED; address requested changes before merge.
  • 7 unresolved current review thread(s) remain.
  • Required check validate naruon image is FAILURE on the current head.
  • Required check validate backend image is FAILURE on the current head.
  • Required check validate frontend image is FAILURE on the current head.
  • Required check frontend is FAILURE on the current head.
  • Required check trivy-fs is FAILURE on the current head.
  • Current-head CodeRabbit review comment has blocking warning/failure evidence on 0615a8f.

Copilot AI 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.

Pull request overview

Hardens the email API surface against SMTP CRLF/header injection and file-upload double-extension bypasses, reducing risk of unauthorized email actions and malicious upload acceptance.

Changes:

  • Added a Pydantic @field_validator(mode="before") to reject CR/LF characters in header-like request fields for /api/emails/send.
  • Reworked /api/emails/import-files filename validation to require an allowed final extension and reject embedded dangerous extensions.
  • Documented the vulnerability and prevention pattern in the Sentinel security log.

Reviewed changes

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

File Description
backend/api/emails.py Adds CRLF rejection for email header fields and strengthens upload filename extension validation.
.jules/sentinel.md Records the vulnerability/learning/prevention notes (but currently duplicates an existing entry).
Comments suppressed due to low confidence (1)

backend/api/emails.py:589

  • Filename validation splits on '.' but doesn't normalize individual segments. A filename like malware.exe .eml (space before the dot) will produce the segment exe which won't match .exe, potentially allowing a double-extension bypass on platforms/tools that trim/normalize whitespace in filenames. Strip each segment (or otherwise normalize) before checking allowed/dangerous extensions.
        if len(segments) < 2 or ("." + segments[-1]) not in allowed_extensions:
            raise HTTPException(status_code=400, detail="invalid_file_type")

        dangerous_extensions = {".exe", ".sh", ".bat", ".cmd", ".msi", ".vbs", ".scr", ".pif", ".dll", ".com"}
        if any(("." + seg) in dangerous_extensions for seg in segments[:-1]):

πŸ’‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/api/emails.py
Comment on lines +583 to +586
allowed_extensions = {".eml", ".zip", ".mbox"}
segments = normalized_filename.split(".")
if len(segments) < 2 or ("." + segments[-1]) not in allowed_extensions:
raise HTTPException(status_code=400, detail="invalid_file_type")
Comment thread backend/api/emails.py
Comment on lines +704 to +713
@field_validator("to", "subject", "in_reply_to", "references", mode="before")
@classmethod
def reject_crlf(cls, v: str | None) -> str | None:
if v is None:
return v
if not isinstance(v, str):
return v
if chr(10) in v or chr(13) in v:
raise ValueError("Email header fields must not contain newlines")
return v
Comment thread .jules/sentinel.md
Comment on lines +117 to +120
## 2024-06-25 - [Fix Email SMTP CRLF Injection & Double Extension Upload]
**Vulnerability:** Attackers could inject arbitrary SMTP commands (e.g. MAIL FROM) using CRLF (\r\n) sequences in email subjects or recipients because `^[^\r\n]*$` validation in Pydantic wasn't catching all edge cases correctly. Attackers could also bypass file upload validations by providing double extensions (e.g., `malicious.exe.eml`).
**Learning:** Pydantic regex patterns might fall short for strict network protocol inputs like SMTP headers if improperly formulated or bypassed. Simple `.endswith()` checks for file uploads fail to prevent embedded dangerous extensions.
**Prevention:** Always use `@field_validator` with explicit `mode="before"` string matching for `chr(10)` and `chr(13)` across all user-controlled email header fields (to, subject, in_reply_to, references). Always tokenize uploaded filenames via `.split(".")` and reject if any segment matches a known dangerous extension (e.g., `.exe`, `.sh`).
이 컀밋은 Trivy νŒŒμΌμ‹œμŠ€ν…œ μŠ€μΊ”μ—μ„œ 발견된 취약점(CVE)을 μˆ˜μ •ν•˜κΈ° μœ„ν•΄ ν”„λ‘ νŠΈμ—”λ“œ νŒ¨ν‚€μ§€λ₯Ό μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.
1. `next` νŒ¨ν‚€μ§€λ₯Ό CVEκ°€ 패치된 16.2.12 λ²„μ „μœΌλ‘œ μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.
2. `sharp` νŒ¨ν‚€μ§€λ₯Ό 0.35.3 λ²„μ „μœΌλ‘œ μ—…λ°μ΄νŠΈν•˜μ—¬ κ΄€λ ¨λœ 취약점을 ν•΄κ²°ν•©λ‹ˆλ‹€.
3. `postcss`λ₯Ό μ—…λ°μ΄νŠΈλœ μ˜μ‘΄μ„±μ— λ§žμΆ”μ–΄ 8.5.23 λ²„μ „μœΌλ‘œ μ„€μ •ν•©λ‹ˆλ‹€.
Copilot AI review requested due to automatic review settings July 27, 2026 14:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

πŸ€– 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 @.jules/sentinel.md:
- Around line 117-120: Remove the duplicate 2024-06-25 sentinel entry from
.jules/sentinel.md, preserving a single authoritative heading and its guidance.
Keep the remaining entry unchanged.

In `@backend/api/emails.py`:
- Around line 584-589: Normalize each segment in the filename validation flow
before comparing against dangerous_extensions, including trimming whitespace or
rejecting whitespace-padded segments so values like β€œexe ” cannot bypass the
check. Update the logic around normalized_filename and the dangerous_extensions
check, and add a regression test covering a filename such as malware.exe .eml.

In `@frontend/package.json`:
- Line 46: Update the PostCSS version constraints in package.json overrides,
package.json resolutions, and the frontend/.pnpmfile.cjs configuration from the
older ranges to 8.5.23, then regenerate the lockfile so all direct and
transitive resolutions align with the patched release.
πŸͺ„ 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e82f096-dec3-4cb3-a511-26e875a8c41f

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 58d19ee and 0615a8f.

β›” Files ignored due to path filters (1)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (3)
  • .jules/sentinel.md
  • backend/api/emails.py
  • frontend/package.json

Comment thread .jules/sentinel.md
Comment on lines +117 to +120
## 2024-06-25 - [Fix Email SMTP CRLF Injection & Double Extension Upload]
**Vulnerability:** Attackers could inject arbitrary SMTP commands (e.g. MAIL FROM) using CRLF (\r\n) sequences in email subjects or recipients because `^[^\r\n]*$` validation in Pydantic wasn't catching all edge cases correctly. Attackers could also bypass file upload validations by providing double extensions (e.g., `malicious.exe.eml`).
**Learning:** Pydantic regex patterns might fall short for strict network protocol inputs like SMTP headers if improperly formulated or bypassed. Simple `.endswith()` checks for file uploads fail to prevent embedded dangerous extensions.
**Prevention:** Always use `@field_validator` with explicit `mode="before"` string matching for `chr(10)` and `chr(13)` across all user-controlled email header fields (to, subject, in_reply_to, references). Always tokenize uploaded filenames via `.split(".")` and reject if any segment matches a known dangerous extension (e.g., `.exe`, `.sh`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Remove the duplicate sentinel entry.

This section duplicates lines 94-97 verbatim, producing duplicate headings and redundant guidance. Keep one authoritative entry.

🧰 Tools
πŸͺ› markdownlint-cli2 (0.23.0)

[warning] 117-117: Multiple headings with the same content

(MD024, no-duplicate-heading)

πŸ€– 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 @.jules/sentinel.md around lines 117 - 120, Remove the duplicate 2024-06-25
sentinel entry from .jules/sentinel.md, preserving a single authoritative
heading and its guidance. Keep the remaining entry unchanged.

Source: Linters/SAST tools

Comment thread backend/api/emails.py
Comment on lines +584 to +589
segments = normalized_filename.split(".")
if len(segments) < 2 or ("." + segments[-1]) not in allowed_extensions:
raise HTTPException(status_code=400, detail="invalid_file_type")

dangerous_extensions = {".exe", ".sh", ".bat", ".cmd", ".msi", ".vbs", ".scr", ".pif", ".dll", ".com"}
if any(("." + seg) in dangerous_extensions for seg in segments[:-1]):

Copy link
Copy Markdown
Contributor

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

Normalize each filename segment before checking dangerous extensions.

strip() only trims the whole filename, so malware.exe .eml produces an earlier segment of exe and bypasses the .exe check. Normalize or reject whitespace-padded segments and add a regression test.

Proposed fix
-        segments = normalized_filename.split(".")
+        segments = [segment.strip() for segment in normalized_filename.split(".")]
πŸ“ 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
segments = normalized_filename.split(".")
if len(segments) < 2 or ("." + segments[-1]) not in allowed_extensions:
raise HTTPException(status_code=400, detail="invalid_file_type")
dangerous_extensions = {".exe", ".sh", ".bat", ".cmd", ".msi", ".vbs", ".scr", ".pif", ".dll", ".com"}
if any(("." + seg) in dangerous_extensions for seg in segments[:-1]):
segments = [segment.strip() for segment in normalized_filename.split(".")]
if len(segments) < 2 or ("." + segments[-1]) not in allowed_extensions:
raise HTTPException(status_code=400, detail="invalid_file_type")
dangerous_extensions = {".exe", ".sh", ".bat", ".cmd", ".msi", ".vbs", ".scr", ".pif", ".dll", ".com"}
if any(("." + seg) in dangerous_extensions for seg in segments[:-1]):
πŸ€– 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 `@backend/api/emails.py` around lines 584 - 589, Normalize each segment in the
filename validation flow before comparing against dangerous_extensions,
including trimming whitespace or rejecting whitespace-padded segments so values
like β€œexe ” cannot bypass the check. Update the logic around normalized_filename
and the dangerous_extensions check, and add a regression test covering a
filename such as malware.exe .eml.

Comment thread frontend/package.json
"fast-check": "^4.9.0",
"jsdom": "^29.1.0",
"postcss": "^8.5.16",
"postcss": "^8.5.23",

Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

cd frontend
rg -n '"postcss"|postcss:' package.json .pnpmfile.cjs
rg -n 'postcss@|version: 8\.5\.' pnpm-lock.yaml
pnpm list postcss --depth Infinity

Repository: ContextualWisdomLab/naruon

Length of output: 1239


🏁 Script executed:

cd frontend
printf '\n== package.json ==\n'
cat -n package.json | sed -n '40,65p'
printf '\n== .pnpmfile.cjs ==\n'
cat -n .pnpmfile.cjs | sed -n '1,120p'
printf '\n== pnpm-lock postcss hits ==\n'
rg -n 'postcss@8\.5\.|version: 8\.5\.|postcss:' pnpm-lock.yaml

Repository: ContextualWisdomLab/naruon

Length of output: 1545


Align all PostCSS constraints with 8.5.23

frontend/package.json already updates the direct dependency, but overrides, resolutions, and frontend/.pnpmfile.cjs still force ^8.5.16/^8.5.15. Those lower ranges can leave transitive installs below the patched release, so bump all three to 8.5.23 and refresh the lockfile.

πŸ€– 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 `@frontend/package.json` at line 46, Update the PostCSS version constraints in
package.json overrides, package.json resolutions, and the frontend/.pnpmfile.cjs
configuration from the older ranges to 8.5.23, then regenerate the lockfile so
all direct and transitive resolutions align with the patched release.

Copilot AI 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.

Pull request overview

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

Files not reviewed (1)
  • frontend/pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (2)

backend/api/emails.py:589

  • This introduces new security behavior (rejecting double extensions like malware.exe.eml), but there is no regression test coverage for the new filename validation. Please add API tests that assert import-files rejects embedded dangerous extensions and accepts valid .eml/.zip/.mbox names.
        if any(("." + seg) in dangerous_extensions for seg in segments[:-1]):

.jules/sentinel.md:120

  • This Sentinel entry is duplicated: the same "2024-06-25 - [Fix Email SMTP CRLF Injection & Double Extension Upload]" section already exists earlier in the file. Please remove the duplicate block to keep the log unambiguous.
## 2024-06-25 - [Fix Email SMTP CRLF Injection & Double Extension Upload]
**Vulnerability:** Attackers could inject arbitrary SMTP commands (e.g. MAIL FROM) using CRLF (\r\n) sequences in email subjects or recipients because `^[^\r\n]*$` validation in Pydantic wasn't catching all edge cases correctly. Attackers could also bypass file upload validations by providing double extensions (e.g., `malicious.exe.eml`).
**Learning:** Pydantic regex patterns might fall short for strict network protocol inputs like SMTP headers if improperly formulated or bypassed. Simple `.endswith()` checks for file uploads fail to prevent embedded dangerous extensions.
**Prevention:** Always use `@field_validator` with explicit `mode="before"` string matching for `chr(10)` and `chr(13)` across all user-controlled email header fields (to, subject, in_reply_to, references). Always tokenize uploaded filenames via `.split(".")` and reject if any segment matches a known dangerous extension (e.g., `.exe`, `.sh`).

Comment thread backend/api/emails.py
raise HTTPException(status_code=400, detail="invalid_file_type")

allowed_extensions = {".eml", ".zip", ".mbox"}
segments = normalized_filename.split(".")
Comment thread frontend/package.json
@seonghobae

Copy link
Copy Markdown
Contributor Author

Superseded by #1206 on current develop (document organization_id scope and/or SMTP CRLF header rejection reimplemented with tests).

@seonghobae seonghobae closed this Jul 31, 2026
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