Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork939
Draft: safe mode to disable executing any external programs except git#2029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -26,6 +26,7 @@ | ||
CommandError, | ||
GitCommandError, | ||
GitCommandNotFound, | ||
UnsafeExecutionError, | ||
UnsafeOptionError, | ||
UnsafeProtocolError, | ||
) | ||
@@ -398,6 +399,7 @@ class Git(metaclass=_GitMeta): | ||
__slots__ = ( | ||
"_working_dir", | ||
"_safe", | ||
"cat_file_all", | ||
"cat_file_header", | ||
"_version_info", | ||
@@ -944,17 +946,20 @@ def __del__(self) -> None: | ||
self._stream.read(bytes_left + 1) | ||
# END handle incomplete read | ||
def __init__(self, working_dir: Union[None, PathLike] = None, safe: bool = False) -> None: | ||
"""Initialize this instance with: | ||
:param working_dir: | ||
Git directory we should work in. If ``None``, we always work in the current | ||
directory as returned by :func:`os.getcwd`. | ||
This is meant to be the working tree directory if available, or the | ||
``.git`` directory in case of bare repositories. | ||
TODO :param safe: | ||
""" | ||
super().__init__() | ||
self._working_dir = expand_path(working_dir) | ||
self._safe = safe | ||
self._git_options: Union[List[str], Tuple[str, ...]] = () | ||
self._persistent_git_options: List[str] = [] | ||
@@ -1201,6 +1206,8 @@ def execute( | ||
:raise git.exc.GitCommandError: | ||
:raise git.exc.UnsafeExecutionError: | ||
:note: | ||
If you add additional keyword arguments to the signature of this method, you | ||
must update the ``execute_kwargs`` variable housed in this module. | ||
@@ -1210,6 +1217,51 @@ def execute( | ||
if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): | ||
_logger.info(" ".join(redacted_command)) | ||
if self._safe: | ||
if isinstance(command, str) or command[0] != self.GIT_PYTHON_GIT_EXECUTABLE: | ||
raise UnsafeExecutionError( | ||
redacted_command, | ||
f'Only {self.GIT_PYTHON_GIT_EXECUTABLE} can be executed when in safe mode.', | ||
) | ||
if shell: | ||
raise UnsafeExecutionError( | ||
redacted_command, | ||
f'Command cannot be executed in a shell when in safe mode.', | ||
) | ||
config_args = [ | ||
Member
| ||
"-c", | ||
"core.askpass=/bin/true", | ||
"-c", | ||
"core.fsmonitor=false", | ||
"-c", | ||
"core.hooksPath=/dev/null", | ||
"-c", | ||
"core.sshCommand=/bin/true", | ||
"-c", | ||
"credential.helper=/bin/true", | ||
"-c", | ||
"http.emptyAuth=true", | ||
"-c", | ||
"protocol.allow=never", | ||
"-c", | ||
"protocol.https.allow=always", | ||
"-c", | ||
"url.https://bitbucket.org/.insteadOf=git@bitbucket.org:", | ||
"-c", | ||
"url.https://codeberg.org/.insteadOf=git@codeberg.org:", | ||
"-c", | ||
"url.https://github.com/.insteadOf=git@github.com:", | ||
"-c", | ||
"url.https://gitlab.com/.insteadOf=git@gitlab.com:", | ||
"-c", | ||
"url.https://.insteadOf=git://", | ||
"-c", | ||
"url.https://.insteadOf=http://", | ||
"-c", | ||
"url.https://.insteadOf=ssh://", | ||
] | ||
command = [command.pop(0)] + config_args + command | ||
# Allow the user to have the command executed in their working dir. | ||
try: | ||
cwd = self._working_dir or os.getcwd() # type: Union[None, str] | ||
@@ -1227,6 +1279,15 @@ def execute( | ||
# just to be sure. | ||
env["LANGUAGE"] = "C" | ||
env["LC_ALL"] = "C" | ||
# Globally disable things that can execute commands, including password prompts. | ||
if self._safe: | ||
env["GIT_ASKPASS"] = "/bin/true" | ||
env["GIT_EDITOR"] = "/bin/true" | ||
env["GIT_PAGER"] = "/bin/true" | ||
env["GIT_SSH"] = "/bin/true" | ||
env["GIT_SSH_COMMAND"] = "/bin/true" | ||
env["GIT_TERMINAL_PROMPT"] = "false" | ||
env["SSH_ASKPASS"] = "/bin/true" | ||
env.update(self._environment) | ||
if inline_env is not None: | ||
env.update(inline_env) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -131,6 +131,9 @@ class Repo: | ||
git_dir: PathLike | ||
"""The ``.git`` repository directory.""" | ||
safe: None | ||
"""Whether this is operating using restricted protocol and execution access.""" | ||
_common_dir: PathLike = "" | ||
# Precompiled regex | ||
@@ -175,6 +178,7 @@ def __init__( | ||
odbt: Type[LooseObjectDB] = GitCmdObjectDB, | ||
search_parent_directories: bool = False, | ||
expand_vars: bool = True, | ||
safe: bool = False, | ||
) -> None: | ||
R"""Create a new :class:`Repo` instance. | ||
@@ -204,6 +208,17 @@ def __init__( | ||
Please note that this was the default behaviour in older versions of | ||
GitPython, which is considered a bug though. | ||
:param safe: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I think without exhaustive tests and actually challenging the code we wouldn't want to make the flag seem like a perfect defence. Author
| ||
Lock down the configuration to make it as safe as possible | ||
when working with publicly accessible, untrusted | ||
repositories. This disables all known options that can run | ||
an external program and limits networking to the HTTP | ||
protocol via https:// URLs. This might not cover Git config | ||
options that were added since this was implemented, or | ||
options that might have unknown exploit vectors. It is a | ||
best effort defense rather than an exhaustive protection | ||
measure. | ||
:raise git.exc.InvalidGitRepositoryError: | ||
:raise git.exc.NoSuchPathError: | ||
@@ -235,6 +250,8 @@ def __init__( | ||
if not os.path.exists(epath): | ||
raise NoSuchPathError(epath) | ||
self.safe = safe | ||
# Walk up the path to find the `.git` dir. | ||
curpath = epath | ||
git_dir = None | ||
@@ -309,7 +326,7 @@ def __init__( | ||
# END working dir handling | ||
self.working_dir: PathLike = self._working_tree_dir or self.common_dir | ||
self.git = self.GitCommandWrapperType(self.working_dir, safe) | ||
# Special handling, in special times. | ||
rootpath = osp.join(self.common_dir, "objects") | ||
@@ -1305,6 +1322,7 @@ def init( | ||
mkdir: bool = True, | ||
odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, | ||
expand_vars: bool = True, | ||
safe: bool = False, | ||
**kwargs: Any, | ||
) -> "Repo": | ||
"""Initialize a git repository at the given path if specified. | ||
@@ -1329,6 +1347,8 @@ def init( | ||
information disclosure, allowing attackers to access the contents of | ||
environment variables. | ||
TODO :param safe: | ||
:param kwargs: | ||
Keyword arguments serving as additional options to the | ||
:manpage:`git-init(1)` command. | ||
@@ -1342,9 +1362,9 @@ def init( | ||
os.makedirs(path, 0o755) | ||
# git command automatically chdir into the directory | ||
git = cls.GitCommandWrapperType(path, safe) | ||
git.init(**kwargs) | ||
return cls(path, odbt=odbt, safe=safe) | ||
@classmethod | ||
def _clone( | ||
@@ -1357,6 +1377,7 @@ def _clone( | ||
multi_options: Optional[List[str]] = None, | ||
allow_unsafe_protocols: bool = False, | ||
allow_unsafe_options: bool = False, | ||
safe: Union[bool, None] = None, | ||
**kwargs: Any, | ||
) -> "Repo": | ||
odbt = kwargs.pop("odbt", odb_default_type) | ||
@@ -1418,7 +1439,11 @@ def _clone( | ||
if not osp.isabs(path): | ||
path = osp.join(git._working_dir, path) if git._working_dir is not None else path | ||
# if safe is not explicitly defined, then the new Repo instance should inherit the safe value | ||
if safe is None: | ||
safe = git._safe | ||
repo = cls(path, odbt=odbt, safe=safe) | ||
# Retain env values that were passed to _clone(). | ||
repo.git.update_environment(**git.environment()) | ||
@@ -1501,6 +1526,7 @@ def clone_from( | ||
multi_options: Optional[List[str]] = None, | ||
allow_unsafe_protocols: bool = False, | ||
allow_unsafe_options: bool = False, | ||
safe: bool = False, | ||
**kwargs: Any, | ||
) -> "Repo": | ||
"""Create a clone from the given URL. | ||
@@ -1531,13 +1557,16 @@ def clone_from( | ||
:param allow_unsafe_options: | ||
Allow unsafe options to be used, like ``--upload-pack``. | ||
:param safe: | ||
TODO | ||
:param kwargs: | ||
See the :meth:`clone` method. | ||
:return: | ||
:class:`Repo` instance pointing to the cloned directory. | ||
""" | ||
git = cls.GitCommandWrapperType(os.getcwd(), safe) | ||
if env is not None: | ||
git.update_environment(**env) | ||
return cls._clone( | ||
@@ -1549,6 +1578,7 @@ def clone_from( | ||
multi_options, | ||
allow_unsafe_protocols=allow_unsafe_protocols, | ||
allow_unsafe_options=allow_unsafe_options, | ||
safe=safe, | ||
**kwargs, | ||
) | ||
Uh oh!
There was an error while loading.Please reload this page.