fix(security): harden SecurityPolicy against quote-based bypasses (SONAR:SEC-001)#758
Conversation
Implements strip_all_quotes to remove all single and double quotes from command arguments and paths before security validation. This prevents bypassing absolute path and forbidden path checks using nested or partial shell quoting (e.g., "/etc"/passwd). - Replaces strip_matching_quotes with strip_all_quotes. - Updates normalize_arg_for_path_checks to strip all quotes. - Updates is_path_allowed to decode before stripping quotes. - Adds comprehensive regression tests for quote-based bypass attempts. - Updates the security journal with findings and mitigations.
Deploying corvus with
|
| Latest commit: |
360176f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e5393174.corvus-42x.pages.dev |
| Branch Preview URL: | https://fix-security-hardening-quote.corvus-42x.pages.dev |
|
👋 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 New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
📝 WalkthroughSummary by CodeRabbitBug Fixes
WalkthroughHardens SecurityPolicy by removing all single/double quotes from URL-decoded paths/arguments before path checks. ChangesSecurity Policy Quote Stripping
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labelsarea:rust, area:docs, risk:security 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Pull request overview
This PR aims to harden SecurityPolicy path validation in clients/agent-runtime against quote-based bypasses that can defeat prefix-/component-based checks (e.g., nested/partial shell quoting).
Changes:
- Introduces
strip_all_quotesand applies it during command argument normalization to defeat quote-splitting bypasses. - Adds regression tests covering quoted/partially-quoted paths and quoted flags.
- Updates the agent journal entry documenting the quote-bypass learning and remediation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
clients/agent-runtime/src/security/policy.rs |
Adds quote-stripping normalization + new tests for quote-based bypass scenarios. |
.agents/journal/sentinnel-journal.md |
Documents the quote-based bypass issue and intended fix. |
Comments suppressed due to low confidence (1)
clients/agent-runtime/src/security/policy.rs:603
dequotedis computed but never used, and all subsequent validation still operates ondecoded/expanded. This keeps the original quote-based bypass inis_path_allowed(e.g."../etc/passwd"won’t hit the ParentDir component check) and will also make the newly addedis_path_allowed_blocks_quoted_pathstest fail. Use the dequoted/normalized string for all checks (null byte, backslash, residual%, traversal, tilde expansion, absolute/forbidden path checks).
let decoded = iterative_url_decode(path);
let dequoted = strip_all_quotes(&decoded);
// Block null bytes (can truncate paths in C-backed syscalls)
if decoded.contains('\0') {
return false;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Yuniel Acosta Pérez <33158051+yacosta738@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Yuniel Acosta Pérez <33158051+yacosta738@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
clients/agent-runtime/src/security/policy.rs (1)
596-636:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winCritical:
dequotedis computed but never used—quote-stripping bypass is not applied.The pipeline warning about
unused variable dequotedreveals the core bug: all security checks still operate ondecoded, notdequoted. The quote-based bypass mitigation is completely ineffective as written.Replace
decodedwithdequotedin subsequent checks:🔒 Proposed fix
pub fn is_path_allowed(&self, path: &str) -> bool { let decoded = iterative_url_decode(path); let dequoted = strip_all_quotes(&decoded); // Block null bytes (can truncate paths in C-backed syscalls) - if decoded.contains('\0') { + if dequoted.contains('\0') { return false; } // Block backslashes (Windows-style separators or escaping) - if decoded.contains('\\') { + if dequoted.contains('\\') { return false; } // Block residual percent signs after decoding (incomplete or malicious encoding) - if decoded.contains('%') { + if dequoted.contains('%') { return false; } // Block path traversal: check for ".." as a path component - if Path::new(&decoded) + if Path::new(&dequoted) .components() .any(|c| matches!(c, std::path::Component::ParentDir)) { return false; } - let expanded = expand_tilde(&decoded); + let expanded = expand_tilde(&dequoted); // Block absolute paths when workspace_only is set if self.workspace_only && Path::new(&expanded).is_absolute() { return false; } // Block forbidden paths using path-component-aware matching if matches_any_forbidden_path(&expanded, &self.forbidden_paths) { return false; } true }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clients/agent-runtime/src/security/policy.rs` around lines 596 - 636, In is_path_allowed, strip_all_quotes(decoded) result (dequoted) is computed but never used, so all security checks still operate on decoded; update the function to perform subsequent checks (null-byte, backslash, '%', path traversal via Path::components(), expand_tilde, workspace_only absolute-path check, and matches_any_forbidden_path) on dequoted (and use expanded = expand_tilde(&dequoted)) instead of decoded so the quote-stripping mitigation takes effect; keep the original sequence of checks and only swap decoded -> dequoted/expanded where appropriate (functions: iterative_url_decode, strip_all_quotes, expand_tilde, matches_any_forbidden_path, and the workspace_only field).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.agents/journal/sentinnel-journal.md:
- Around line 20-24: The journal entry titled "2025-05-28 - Hardening
SecurityPolicy against Quote-based Bypasses" is out of chronological order;
locate that header and its entire block (the Learning and Action paragraphs
about strip_all_quotes and iterative_url_decode) and move it so it appears after
the existing entries for 2025-05-20 and 2025-05-24, preserving the block content
and blank-line separation so the file remains chronologically ordered.
In `@clients/agent-runtime/src/security/policy.rs`:
- Around line 2201-2211: In is_path_allowed, the dequoted variable is created
but never used, so path checks still operate on the original quoted string;
update the function to use dequoted (not decoded or the original input) for the
path validation calls—specifically pass dequoted into is_absolute() and
matches_any_forbidden_path() (and any other subsequent path checks) so quoted
inputs are dequoted before validation and the unused dequoted variable is
actually used.
---
Outside diff comments:
In `@clients/agent-runtime/src/security/policy.rs`:
- Around line 596-636: In is_path_allowed, strip_all_quotes(decoded) result
(dequoted) is computed but never used, so all security checks still operate on
decoded; update the function to perform subsequent checks (null-byte, backslash,
'%', path traversal via Path::components(), expand_tilde, workspace_only
absolute-path check, and matches_any_forbidden_path) on dequoted (and use
expanded = expand_tilde(&dequoted)) instead of decoded so the quote-stripping
mitigation takes effect; keep the original sequence of checks and only swap
decoded -> dequoted/expanded where appropriate (functions: iterative_url_decode,
strip_all_quotes, expand_tilde, matches_any_forbidden_path, and the
workspace_only field).
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b80df6b1-f644-48fe-a171-5f1f0ada83de
📒 Files selected for processing (2)
.agents/journal/sentinnel-journal.mdclients/agent-runtime/src/security/policy.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: pr-checks
- GitHub Check: Cloudflare Pages
🧰 Additional context used
📓 Path-based instructions (6)
clients/agent-runtime/src/{security,gateway,tools}/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
Treat
src/security/,src/gateway/,src/tools/as high-risk surfaces and never broaden filesystem/network execution scope without explicit policy checks
Files:
clients/agent-runtime/src/security/policy.rs
clients/agent-runtime/src/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
clients/agent-runtime/src/**/*.rs: Never log secrets, tokens, raw credentials, or sensitive payloads in any logging statements
Avoid unnecessary allocations, clones, and blocking operations to maintain performance and efficiency
Files:
clients/agent-runtime/src/security/policy.rs
clients/agent-runtime/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
Run
cargo fmt --all -- --check,cargo clippy --all-targets -- -D warnings, andcargo testfor code validation, or document which checks were skipped and why
Files:
clients/agent-runtime/src/security/policy.rs
clients/agent-runtime/src/{security,gateway,tools,config}/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
Do not silently weaken security policy or access constraints; keep default behavior secure-by-default with deny-by-default where applicable
Files:
clients/agent-runtime/src/security/policy.rs
**/*.rs
⚙️ CodeRabbit configuration file
**/*.rs: Focus on Rust idioms, memory safety, and ownership/borrowing correctness.
Flag unnecessary clones, unchecked panics in production paths, and weak error context.
Prioritize unsafe blocks, FFI boundaries, concurrency races, and secret handling.
Files:
clients/agent-runtime/src/security/policy.rs
**/*
⚙️ CodeRabbit configuration file
**/*: Security first, performance second.
Validate input boundaries, auth/authz implications, and secret management.
Look for behavioral regressions, missing tests, and contract breaks across modules.
Files:
clients/agent-runtime/src/security/policy.rs
🧠 Learnings (1)
📚 Learning: 2026-02-21T09:07:52.298Z
Learnt from: yacosta738
Repo: dallay/corvus PR: 62
File: .agents/journal/sentinnel-journal.md:1-1
Timestamp: 2026-02-21T09:07:52.298Z
Learning: Branding guideline: The intentional brand name for the security-first agent in the dallay/corvus repository is 'Sentinnel' (with double n). Do not treat it as a typo of 'Sentinel'. Ensure all agent-related docs and journals under .agents/journal consistently use 'Sentinnel' with double n.
Applied to files:
.agents/journal/sentinnel-journal.md
🪛 GitHub Actions: SonarQube Analysis
clients/agent-runtime/src/security/policy.rs
[warning] 598-598: Rust warning: unused variable dequoted. help: if this is intentional, prefix it with an underscore: _dequoted (#[warn(unused_variables)] on by default)
🔇 Additional comments (2)
clients/agent-runtime/src/security/policy.rs (2)
815-817: Implementation is correct; be aware of semantic change for filenames with literal quotes.Stripping all quotes changes validation semantics for paths like
my"file.txt(the rare case where quotes are part of the actual filename). This is an acceptable trade-off for closing the bypass vector, but call sites infile_read.rs:55andscheduler.rs:335should be aware that the validated path may differ from the literal input.
819-831: LGTM!Quote stripping is correctly applied and
dequotedis used consistently throughout this function.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 2 file(s) based on 2 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@clients/agent-runtime/src/security/policy.rs`:
- Around line 596-623: The is_path_allowed function currently decodes and strips
quotes (uses iterative_url_decode and strip_all_quotes) so its checks are run on
a transformed path; revert it to validate the exact raw path argument instead:
remove or skip the dequoting/decoding steps in is_path_allowed and perform all
checks (null byte, backslash, '%', ParentDir detection) directly on the original
path parameter, and move any shell-style normalization (quote
stripping/URL-decoding) into normalize_arg_for_path_checks so shell-parsed
inputs are handled upstream; keep references to iterative_url_decode and
strip_all_quotes only in normalize_arg_for_path_checks and ensure callers that
rely on normalized values use that normalized output consistently.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 183d67d6-5688-40c2-8dd3-fbc4dd156427
📒 Files selected for processing (2)
.agents/journal/sentinnel-journal.mdclients/agent-runtime/src/security/policy.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: sonar
- GitHub Check: pr-checks
- GitHub Check: semgrep-cloud-platform/scan
- GitHub Check: submit-gradle
🧰 Additional context used
📓 Path-based instructions (6)
clients/agent-runtime/src/{security,gateway,tools}/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
Treat
src/security/,src/gateway/,src/tools/as high-risk surfaces and never broaden filesystem/network execution scope without explicit policy checks
Files:
clients/agent-runtime/src/security/policy.rs
clients/agent-runtime/src/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
clients/agent-runtime/src/**/*.rs: Never log secrets, tokens, raw credentials, or sensitive payloads in any logging statements
Avoid unnecessary allocations, clones, and blocking operations to maintain performance and efficiency
Files:
clients/agent-runtime/src/security/policy.rs
clients/agent-runtime/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
Run
cargo fmt --all -- --check,cargo clippy --all-targets -- -D warnings, andcargo testfor code validation, or document which checks were skipped and why
Files:
clients/agent-runtime/src/security/policy.rs
clients/agent-runtime/src/{security,gateway,tools,config}/**/*.rs
📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)
Do not silently weaken security policy or access constraints; keep default behavior secure-by-default with deny-by-default where applicable
Files:
clients/agent-runtime/src/security/policy.rs
**/*.rs
⚙️ CodeRabbit configuration file
**/*.rs: Focus on Rust idioms, memory safety, and ownership/borrowing correctness.
Flag unnecessary clones, unchecked panics in production paths, and weak error context.
Prioritize unsafe blocks, FFI boundaries, concurrency races, and secret handling.
Files:
clients/agent-runtime/src/security/policy.rs
**/*
⚙️ CodeRabbit configuration file
**/*: Security first, performance second.
Validate input boundaries, auth/authz implications, and secret management.
Look for behavioral regressions, missing tests, and contract breaks across modules.
Files:
clients/agent-runtime/src/security/policy.rs
🧠 Learnings (1)
📚 Learning: 2026-02-21T09:07:52.298Z
Learnt from: yacosta738
Repo: dallay/corvus PR: 62
File: .agents/journal/sentinnel-journal.md:1-1
Timestamp: 2026-02-21T09:07:52.298Z
Learning: Branding guideline: The intentional brand name for the security-first agent in the dallay/corvus repository is 'Sentinnel' (with double n). Do not treat it as a typo of 'Sentinel'. Ensure all agent-related docs and journals under .agents/journal consistently use 'Sentinnel' with double n.
Applied to files:
.agents/journal/sentinnel-journal.md
| pub fn is_path_allowed(&self, path: &str) -> bool { | ||
| let decoded = iterative_url_decode(path); | ||
| let dequoted = strip_all_quotes(&decoded); | ||
|
|
||
| // Block null bytes (can truncate paths in C-backed syscalls) | ||
| if decoded.contains('\0') { | ||
| if dequoted.contains('\0') { | ||
| return false; | ||
| } | ||
|
|
||
| // Block backslashes (Windows-style separators or escaping) | ||
| if decoded.contains('\\') { | ||
| if dequoted.contains('\\') { | ||
| return false; | ||
| } | ||
|
|
||
| // Block residual percent signs after decoding (incomplete or malicious encoding) | ||
| if decoded.contains('%') { | ||
| if dequoted.contains('%') { | ||
| return false; | ||
| } | ||
|
|
||
| // Block path traversal: check for ".." as a path component | ||
| if Path::new(&decoded) | ||
| if Path::new(&dequoted) | ||
| .components() | ||
| .any(|c| matches!(c, std::path::Component::ParentDir)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| let expanded = expand_tilde(&decoded); | ||
| let expanded = expand_tilde(&dequoted); |
There was a problem hiding this comment.
Keep is_path_allowed validating the exact raw path.
Line 598 makes this API authorize a dequoted string, but direct file tools use is_path_allowed on raw user input before doing filesystem I/O. That means a literal filename like "notes"/q1.txt is checked as notes/q1.txt, so the policy decision no longer matches the path the tool will actually open. Keep quote stripping in normalize_arg_for_path_checks for shell-command parsing, and leave is_path_allowed operating on the exact path passed by direct file APIs.
Suggested direction
pub fn is_path_allowed(&self, path: &str) -> bool {
let decoded = iterative_url_decode(path);
- let dequoted = strip_all_quotes(&decoded);
// Block null bytes (can truncate paths in C-backed syscalls)
- if dequoted.contains('\0') {
+ if decoded.contains('\0') {
return false;
}
// Block backslashes (Windows-style separators or escaping)
- if dequoted.contains('\\') {
+ if decoded.contains('\\') {
return false;
}
// Block residual percent signs after decoding (incomplete or malicious encoding)
- if dequoted.contains('%') {
+ if decoded.contains('%') {
return false;
}
// Block path traversal: check for ".." as a path component
- if Path::new(&dequoted)
+ if Path::new(&decoded)
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return false;
}
- let expanded = expand_tilde(&dequoted);
+ let expanded = expand_tilde(&decoded);If you still want shell-style quoted paths blocked, do that in the shell-argument normalization path and have direct file tools either reject quotes explicitly upstream or use the normalized value consistently for the subsequent I/O.
As per coding guidelines, clients/agent-runtime/src/{security,gateway,tools}/**/*.rs is a high-risk surface and must never broaden filesystem execution scope without explicit policy checks.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clients/agent-runtime/src/security/policy.rs` around lines 596 - 623, The
is_path_allowed function currently decodes and strips quotes (uses
iterative_url_decode and strip_all_quotes) so its checks are run on a
transformed path; revert it to validate the exact raw path argument instead:
remove or skip the dequoting/decoding steps in is_path_allowed and perform all
checks (null byte, backslash, '%', ParentDir detection) directly on the original
path parameter, and move any shell-style normalization (quote
stripping/URL-decoding) into normalize_arg_for_path_checks so shell-parsed
inputs are handled upstream; keep references to iterative_url_decode and
strip_all_quotes only in normalize_arg_for_path_checks and ensure callers that
rely on normalized values use that normalized output consistently.
|



This PR hardens the
SecurityPolicyin theagent-runtimeagainst a class of path validation bypasses that use shell quoting to disrupt string-based prefix checks.Security Impact
workspace_onlyandforbidden_pathsrestrictions via internal quoting (e.g.,cat "/et""c/passwd").Performance Impact
strip_matching_quotesperformed basic length and character checks.strip_all_quotesuses two string replacements.Verification
clients/agent-runtime/src/security/policy.rs.PR created automatically by Jules for task 6869014117684958430 started by @yacosta738