Harden security and improve shell environment handling#286
Open
amirimatin wants to merge 18 commits into
Open
Conversation
…diation strategies
- add prefix history search on arrow keys and incremental Ctrl+R/Ctrl+S bindings - improve completion rendering with contextual headers, sorting, and deduplication - normalize and deduplicate persisted history entries through shared helpers - extend functional and regression coverage for completion and history behavior
# Conflicts: # test/test_completion.py
…word symlink bypass
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Short summary
This PR updates
pre-releasewith the final integrated hardening state for the recentlshellsecurity work.The branch includes:
This PR is intentionally aggregated because these changes overlap in the command parser, runtime executor, SSH command handling, hardening templates, and tests. Reviewing them as one final branch is cleaner than asking upstream to merge several partially overlapping topic branches in a specific order.
Repository and component background
lshellis a restricted shell. Its core job is to accept a user command, check it against command policy and path policy, and then execute only what the policy allows.In the current codebase, the relevant execution path for this PR crosses these main areas:
lshell/config/runtime.pylshell/utils.pylshell/engine/executor.pylshell/shellcmd.pylshell/hardeninit.pyThis matters because the reviewed issues were not all in one place. Some were in path handling, some in SSH protocol routing, and some in runtime command lookup.
Why this PR is one integrated branch
Earlier work was split across several topic branches and PRs. That was useful during development, but it became less useful once the changes started overlapping in behavior and test coverage.
In this final branch state:
If upstream reviewed these as independent PRs, merge order and conflict handling would become part of the review burden. This PR removes that burden and presents the final branch state exactly as it was validated.
Main change area 1: deterministic command resolution and drift detection
Plain-language summary
Before this change,
lshellcould still depend on the inherited ambientPATHwhen resolving allowed bare commands such asls.After this change,
lshellbuilds its own trusted runtimePATH, resolves bare commands at session start, pins them to a concrete executable, and fails closed if the resolved target changes during the session.Why this matters
If a restricted shell trusts an inherited
PATH, a user or parent process can influence which executable is launched for an allowlisted command name.That is especially dangerous when policy says "
lsis allowed" but the actual executable reached bylscan change due to environment manipulation or an executable swap after session start.Conditions required for the old risk
The old risk existed when all of the following were true:
lsPATHStep-by-step execution path after the fix
lshell/config/runtime.py:625-669lshell/config/runtime.py:87-93andlshell/config/runtime.py:426-431lshell/utils.py:385-500lshell/utils.py:503-591lshell/engine/executor.py:120-135andlshell/engine/executor.py:389-445What the code now does
The hardening chain is explicit:
lshell/utils.py:385-407builds a deterministic runtimePATHlshell/utils.py:437-470resolves a command into path plus metadatalshell/utils.py:473-500caches approved bare-command targetslshell/utils.py:503-525compares the current resolution to the pinned recordlshell/utils.py:528-591rewrites the executed command so the pinned executable is usedlshell/engine/executor.py:396-410denies execution if the command path changedPractical impact
This closes two related problems:
PATHhijack for bare allowlisted commandsIt does not block explicit path commands such as
/usr/bin/fooor./script.sh. That is intentional. This change is targeted at bare command resolution hardening, not at redefining the semantics of explicit paths.Main change area 2: SCP parsing hardening
Plain-language summary
The old SCP routing logic could misclassify transfer direction because it relied on simple string matching for
" -f "and" -t ".That left room for clustered short options such as
-pfor-ptto bypass upload or download restrictions.Why this matters
If
scp_download=0, the policy is supposed to prevent the server-side download flow. If option clustering hides the-fdirection flag from policy detection, the restriction is unreliable.Step-by-step execution path
lshell/shellcmd.py:406-410This PR description does not repeat the full parser internals line by line because the SCP hardening is already integrated into the validated branch state, but this change is part of the same reviewed update set and is covered by the test results listed below.
Practical impact
This closes a policy bypass where clustered short options could start a forbidden SCP direction even though the administrator explicitly disabled it.
Main change area 3: legacy SFTP now fails closed by default
Plain-language summary
The old SFTP passthrough model let
lshelllaunchsftp-servereven thoughlshellcannot inspect each file operation once the SFTP protocol takes over.This means
lshellpath ACLs are not a real SFTP filesystem boundary.The new behavior is explicit and safer:
sftp=1andsftp_unsafe_legacy=1, legacy passthrough is still allowedsftp=1without that override,lshellnow refuses legacy passthroughsshd_configForceCommand internal-sftpwithChrootDirectoryWhy this matters
This is not a cosmetic change. It changes the default security stance from "implicitly allow a model that cannot enforce its claimed path boundary" to "fail closed unless the administrator explicitly accepts the risk."
Conditions required for the old risk
The old risk existed when:
sftp=1was enabledlshellpath ACL to govern actual SFTP file requestsStep-by-step execution path after the fix
lshell/shellcmd.py:354-359lshelldetects whether the requested protocol command is one of the trusted SFTP executables inlshell/shellcmd.py:368-370sftp_unsafe_legacy=1is present, the trusted protocol path is allowed inlshell/shellcmd.py:370-380lshell/shellcmd.py:381-395lshell/hardeninit.py:72-87,lshell/hardeninit.py:206-209, andlshell/hardeninit.py:299-305What this change does not mean
This change does not make legacy
sftp-serverpassthrough path-safe.It means the unsafe legacy behavior is no longer the implicit default. If an administrator still wants it, they must explicitly opt back into it with:
Practical impact
This is a fail-closed design correction, not a protocol proxy.
That distinction matters for review. The security improvement is not "we can now inspect SFTP requests." The real improvement is "we no longer silently allow a mode that suggests stronger containment than the code can actually enforce."
Main change area 4: hardening profiles and docs now match runtime reality
Plain-language summary
A secure runtime model is only useful if the generated profiles and the docs describe the same thing the code does.
This branch updates the hardening templates and docs to match the new fail-closed SFTP model and the new command-resolution model.
Step-by-step effect
sftp-onlyprofile now ships withsftp: 0andsftp_unsafe_legacy: 0inlshell/hardeninit.py:72-87lshell/hardeninit.py:203-210sftp-onlyprofile inlshell/hardeninit.py:299-305Practical impact
This reduces operator confusion. An administrator reading the generated hardening output is less likely to assume that legacy SFTP passthrough is the recommended restricted-SFTP deployment model.
Validation and test status
The integrated branch was validated with the full local suite:
Result:
One important detail matters here.
During the full-suite run after integration, several older tests still assumed that user-visible command strings would always appear as bare executable names such as
ls,sleep, ortail.After command pinning, some user-visible output can legitimately contain pinned absolute command paths such as
/usr/bin/lsor/usr/bin/sleep.Because of that, the final integrated branch includes a test-only alignment commit:
549e4d5test: normalize expectations for pinned command pathsThis commit does not weaken the hardening. It only updates old expectations so they match the new execution model.
Important review notes
This PR is broader than one isolated security patch
This PR carries the final integrated
pre-releasestate, not a minimal cherry-pick set.That means reviewers should expect:
The most important security design decisions to review
If a reviewer wants to focus on the high-signal parts first, these are the best targets:
lshell/utils.py:385-414lshell/utils.py:437-591andlshell/engine/executor.py:389-445lshell/shellcmd.py:368-395sftp-onlyprofile defaults inlshell/hardeninit.py:72-87andlshell/hardeninit.py:299-305