You've already forked httpie-cli
							
							
				mirror of
				https://github.com/httpie/cli.git
				synced 2025-10-30 23:47:52 +02:00 
			
		
		
		
	Apply suggestions from the review
This commit is contained in:
		
							
								
								
									
										10
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| # Security Policy | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
| To report a vulnerability, please send an email to `security@httpie.io` describing the: | ||||
|  | ||||
| - The description of the vulnerability itself | ||||
| - A short reproducer to verify it (you can submit a small HTTP server, a shell script, a docker image etc.) | ||||
| - The severity level classification (`LOW`/`MEDIUM`/`HIGH`/`CRITICAL`) | ||||
| - If associated with any, the [CWE](https://cwe.mitre.org/) ID. | ||||
| @@ -2422,23 +2422,6 @@ And since there’s neither data nor `EOF`, it will get stuck. So unless you’r | ||||
| By default, the plugins (and their missing dependencies) will be stored under the configuration directory, | ||||
| but this can be modified through `plugins_dir` variable on the config. | ||||
|  | ||||
| > the HTTPie project. We do not control / review them at the moment, so use them at your own discretion. | ||||
|  | ||||
| For managing these plugins; starting with 3.0, we are offering a new plugin manager. | ||||
|  | ||||
| This command is currently in beta. | ||||
|  | ||||
| ### `httpie plugins` | ||||
|  | ||||
| `plugins` interface is a very simple plugin manager for installing, listing and uninstalling HTTPie plugins. | ||||
|  | ||||
| In the past `pip` was used to install/uninstall plugins, but on some environments (e.g., brew installed | ||||
| packages) it wasn’t working properly. The new interface is a very simple overlay on top of `pip` to allow | ||||
| plugin installations on every installation method. | ||||
|  | ||||
| By default, the plugins (and their missing dependencies) will be stored under the configuration directory, | ||||
| but this can be modified through `plugins_dir` variable on the config. | ||||
|  | ||||
| #### `httpie plugins install` | ||||
|  | ||||
| For installing plugins from [PyPI](https://pypi.org/) or from local paths, `httpie plugins install` | ||||
|   | ||||
							
								
								
									
										0
									
								
								httpie/legacy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								httpie/legacy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										103
									
								
								httpie/legacy/cookie_format.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								httpie/legacy/cookie_format.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| import argparse | ||||
| from typing import Any, Type, List, Dict, TYPE_CHECKING | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from httpie.sessions import Session | ||||
|  | ||||
| INSECURE_COOKIE_JAR_WARNING = '''\ | ||||
| Outdated layout detected for the current session. Please consider updating it, | ||||
| in order to not get affected by potential security problems. | ||||
|  | ||||
| For fixing the current session: | ||||
|  | ||||
|     With binding all cookies to the current host (secure): | ||||
|         $ httpie cli sessions upgrade --bind-cookies {hostname} {session_id} | ||||
|  | ||||
|     Without binding cookies (leaving them as is) (insecure): | ||||
|         $ httpie cli sessions upgrade {hostname} {session_id} | ||||
| ''' | ||||
|  | ||||
|  | ||||
| INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS = '''\ | ||||
|  | ||||
| For fixing all named sessions: | ||||
|  | ||||
|     With binding all cookies to the current host (secure): | ||||
|         $ httpie cli sessions upgrade-all --bind-cookies | ||||
|  | ||||
|     Without binding cookies (leaving them as is) (insecure): | ||||
|         $ httpie cli sessions upgrade-all | ||||
| ''' | ||||
|  | ||||
| INSECURE_COOKIE_SECURITY_LINK = '\nSee https://pie.co/docs/security for more information.' | ||||
|  | ||||
|  | ||||
| def pre_process(session: 'Session', cookies: Any) -> List[Dict[str, Any]]: | ||||
|     """Load the given cookies to the cookie jar while maintaining | ||||
|     support for the old cookie layout.""" | ||||
|  | ||||
|     is_old_style = isinstance(cookies, dict) | ||||
|     if is_old_style: | ||||
|         normalized_cookies = [ | ||||
|             { | ||||
|                 'name': key, | ||||
|                 **value | ||||
|             } | ||||
|             for key, value in cookies.items() | ||||
|         ] | ||||
|     else: | ||||
|         normalized_cookies = cookies | ||||
|  | ||||
|     should_issue_warning = is_old_style and any( | ||||
|         cookie.get('domain', '') == '' | ||||
|         for cookie in normalized_cookies | ||||
|     ) | ||||
|  | ||||
|     if should_issue_warning and not session.refactor_mode: | ||||
|         warning = INSECURE_COOKIE_JAR_WARNING.format(hostname=session.bound_host, session_id=session.session_id) | ||||
|         if not session.is_anonymous: | ||||
|             warning += INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS | ||||
|         warning += INSECURE_COOKIE_SECURITY_LINK | ||||
|  | ||||
|         session.env.log_error( | ||||
|             warning, | ||||
|             level='warning' | ||||
|         ) | ||||
|  | ||||
|     return normalized_cookies | ||||
|  | ||||
|  | ||||
| def post_process( | ||||
|     normalized_cookies: List[Dict[str, Any]], | ||||
|     *, | ||||
|     original_type: Type[Any] | ||||
| ) -> Any: | ||||
|     """Convert the cookies to their original format for | ||||
|     maximum compatibility.""" | ||||
|  | ||||
|     if issubclass(original_type, dict): | ||||
|         return { | ||||
|             cookie.pop('name'): cookie | ||||
|             for cookie in normalized_cookies | ||||
|         } | ||||
|     else: | ||||
|         return normalized_cookies | ||||
|  | ||||
|  | ||||
| def fix_layout(session: 'Session', hostname: str, args: argparse.Namespace) -> None: | ||||
|     if not isinstance(session['cookies'], dict): | ||||
|         return None | ||||
|  | ||||
|     session['cookies'] = [ | ||||
|         { | ||||
|             'name': key, | ||||
|             **value | ||||
|         } | ||||
|         for key, value in session['cookies'].items() | ||||
|     ] | ||||
|     for cookie in session.cookies: | ||||
|         if cookie.domain == '': | ||||
|             if args.bind_cookies: | ||||
|                 cookie.domain = hostname | ||||
|             else: | ||||
|                 cookie._rest['is_explicit_none'] = True | ||||
| @@ -4,7 +4,7 @@ from httpie import __version__ | ||||
|  | ||||
| CLI_SESSION_UPGRADE_FLAGS = [ | ||||
|     { | ||||
|         'variadic': ['--bind-cookies'], | ||||
|         'flags': ['--bind-cookies'], | ||||
|         'action': 'store_true', | ||||
|         'default': False, | ||||
|         'help': 'Bind domainless cookies to the host that session belongs.' | ||||
| @@ -102,8 +102,8 @@ def generate_subparsers(root, parent_parser, definitions): | ||||
|  | ||||
|         for argument in properties: | ||||
|             argument = argument.copy() | ||||
|             variadic = argument.pop('variadic', []) | ||||
|             command_parser.add_argument(*variadic, **argument) | ||||
|             flags = argument.pop('flags', []) | ||||
|             command_parser.add_argument(*flags, **argument) | ||||
|  | ||||
|  | ||||
| parser = HTTPieManagerArgumentParser( | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import argparse | ||||
| from typing import TypeVar, Callable, Tuple | ||||
|  | ||||
| from httpie.sessions import SESSIONS_DIR_NAME, Session, get_httpie_session | ||||
| from httpie.sessions import SESSIONS_DIR_NAME, get_httpie_session | ||||
| from httpie.status import ExitStatus | ||||
| from httpie.context import Environment | ||||
| from httpie.legacy import cookie_format as legacy_cookies | ||||
| from httpie.manager.cli import missing_subcommand, parser | ||||
|  | ||||
| T = TypeVar('T') | ||||
| @@ -51,27 +52,8 @@ def is_version_greater(version_1: str, version_2: str) -> bool: | ||||
|     return split_version(version_1) > split_version(version_2) | ||||
|  | ||||
|  | ||||
| def fix_cookie_layout(session: Session, hostname: str, args: argparse.Namespace) -> None: | ||||
|     if not isinstance(session['cookies'], dict): | ||||
|         return None | ||||
|  | ||||
|     session['cookies'] = [ | ||||
|         { | ||||
|             'name': key, | ||||
|             **value | ||||
|         } | ||||
|         for key, value in session['cookies'].items() | ||||
|     ] | ||||
|     for cookie in session.cookies: | ||||
|         if cookie.domain == '': | ||||
|             if args.bind_cookies: | ||||
|                 cookie.domain = hostname | ||||
|             else: | ||||
|                 cookie._rest['is_explicit_none'] = True | ||||
|  | ||||
|  | ||||
| FIXERS_TO_VERSIONS = { | ||||
|     '3.1.0': fix_cookie_layout | ||||
|     '3.1.0': legacy_cookies.fix_layout | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import re | ||||
| from http.cookies import SimpleCookie | ||||
| from http.cookiejar import Cookie | ||||
| from pathlib import Path | ||||
| from typing import Any, Dict, Optional, Union | ||||
| from typing import Any, Dict, List, Optional, Union | ||||
|  | ||||
| from requests.auth import AuthBase | ||||
| from requests.cookies import RequestsCookieJar, remove_cookie_by_name | ||||
| @@ -18,6 +18,7 @@ from .cli.dicts import HTTPHeadersDict | ||||
| from .config import BaseConfigDict, DEFAULT_CONFIG_DIR | ||||
| from .utils import url_as_host | ||||
| from .plugins.registry import plugin_manager | ||||
| from .legacy import cookie_format as legacy_cookies | ||||
|  | ||||
|  | ||||
| SESSIONS_DIR_NAME = 'sessions' | ||||
| @@ -32,37 +33,25 @@ SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-'] | ||||
| KEPT_COOKIE_OPTIONS = ['name', 'expires', 'path', 'value', 'domain', 'secure'] | ||||
| DEFAULT_COOKIE_PATH = '/' | ||||
|  | ||||
| INSECURE_COOKIE_JAR_WARNING = '''\ | ||||
| Outdated layout detected for the current session. Please consider updating it, | ||||
| in order to not get affected by potential security problems. | ||||
|  | ||||
| For fixing the current session: | ||||
|  | ||||
|     With binding all cookies to the current host (secure): | ||||
|         $ httpie cli sessions upgrade --bind-cookies {hostname} {session_id} | ||||
|  | ||||
|     Without binding cookies (leaving them as is) (insecure): | ||||
|         $ httpie cli sessions upgrade {hostname} {session_id} | ||||
| ''' | ||||
|  | ||||
| INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS = '''\ | ||||
|  | ||||
| For fixing all named sessions: | ||||
|  | ||||
|     With binding all cookies to the current host (secure): | ||||
|         $ httpie cli sessions upgrade-all --bind-cookies | ||||
|  | ||||
|     Without binding cookies (leaving them as is) (insecure): | ||||
|         $ httpie cli sessions upgrade-all | ||||
|  | ||||
| See https://pie.co/docs/security for more information. | ||||
| ''' | ||||
|  | ||||
|  | ||||
| def is_anonymous_session(session_name: str) -> bool: | ||||
|     return os.path.sep in session_name | ||||
|  | ||||
|  | ||||
| def session_hostname_to_dirname(hostname: str, session_name: str) -> str: | ||||
|     # host:port => host_port | ||||
|     hostname = hostname.replace(':', '_') | ||||
|     return os.path.join( | ||||
|         SESSIONS_DIR_NAME, | ||||
|         hostname, | ||||
|         f'{session_name}.json' | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def strip_port(hostname: str) -> str: | ||||
|     return hostname.split(':')[0] | ||||
|  | ||||
|  | ||||
| def materialize_cookie(cookie: Cookie) -> Dict[str, Any]: | ||||
|     materialized_cookie = { | ||||
|         option: getattr(cookie, option) | ||||
| @@ -92,22 +81,18 @@ def get_httpie_session( | ||||
|         # HACK/FIXME: httpie-unixsocket's URLs have no hostname. | ||||
|         bound_hostname = 'localhost' | ||||
|  | ||||
|     # host:port => host_port | ||||
|     hostname = bound_hostname.replace(':', '_') | ||||
|     if is_anonymous_session(session_name): | ||||
|         path = os.path.expanduser(session_name) | ||||
|         session_id = path | ||||
|     else: | ||||
|         path = ( | ||||
|             config_dir / SESSIONS_DIR_NAME / hostname / f'{session_name}.json' | ||||
|         ) | ||||
|         path = config_dir / session_hostname_to_dirname(bound_hostname, session_name) | ||||
|         session_id = session_name | ||||
|  | ||||
|     session = Session( | ||||
|         path, | ||||
|         env=env, | ||||
|         session_id=session_id, | ||||
|         bound_host=bound_hostname.split(':')[0], | ||||
|         bound_host=strip_port(bound_hostname), | ||||
|         refactor_mode=refactor_mode | ||||
|     ) | ||||
|     session.load() | ||||
| @@ -142,60 +127,35 @@ class Session(BaseConfigDict): | ||||
|  | ||||
|     def pre_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]: | ||||
|         cookies = data.get('cookies') | ||||
|         if isinstance(cookies, dict): | ||||
|             normalized_cookies = [ | ||||
|                 { | ||||
|                     'name': key, | ||||
|                     **value | ||||
|                 } | ||||
|                 for key, value in cookies.items() | ||||
|             ] | ||||
|         elif isinstance(cookies, list): | ||||
|             normalized_cookies = cookies | ||||
|         if cookies: | ||||
|             normalized_cookies = legacy_cookies.pre_process(self, cookies) | ||||
|         else: | ||||
|             normalized_cookies = [] | ||||
|  | ||||
|         should_issue_warning = False | ||||
|         for cookie in normalized_cookies: | ||||
|             domain = cookie.get('domain', '') | ||||
|             if domain == '' and isinstance(cookies, dict): | ||||
|                 should_issue_warning = True | ||||
|             elif domain is None: | ||||
|             if domain is None: | ||||
|                 # domain = None means explicitly lack of cookie, though | ||||
|                 # requests requires domain to be string so we'll cast it | ||||
|                 # requests requires domain to be a string so we'll cast it | ||||
|                 # manually. | ||||
|                 cookie['domain'] = '' | ||||
|                 cookie['rest'] = {'is_explicit_none': True} | ||||
|  | ||||
|             self.cookie_jar.set(**cookie) | ||||
|  | ||||
|         if should_issue_warning and not self.refactor_mode: | ||||
|             warning = INSECURE_COOKIE_JAR_WARNING.format(hostname=self.bound_host, session_id=self.session_id) | ||||
|             if not is_anonymous_session(self.session_id): | ||||
|                 warning += INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS | ||||
|  | ||||
|             self.env.log_error( | ||||
|                 warning, | ||||
|                 level='warning' | ||||
|             ) | ||||
|  | ||||
|         return data | ||||
|  | ||||
|     def post_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]: | ||||
|         cookies = data.get('cookies') | ||||
|         # Save in the old-style fashion | ||||
|  | ||||
|         normalized_cookies = [ | ||||
|             materialize_cookie(cookie) | ||||
|             for cookie in self.cookie_jar | ||||
|         ] | ||||
|         if isinstance(cookies, dict): | ||||
|             data['cookies'] = { | ||||
|                 cookie.pop('name'): cookie | ||||
|                 for cookie in normalized_cookies | ||||
|             } | ||||
|         else: | ||||
|             data['cookies'] = normalized_cookies | ||||
|         data['cookies'] = legacy_cookies.post_process( | ||||
|             normalized_cookies, | ||||
|             original_type=type(cookies) | ||||
|         ) | ||||
|  | ||||
|         return data | ||||
|  | ||||
| @@ -251,7 +211,7 @@ class Session(BaseConfigDict): | ||||
|     def cookies(self, jar: RequestsCookieJar): | ||||
|         self.cookie_jar = jar | ||||
|  | ||||
|     def remove_cookies(self, cookies: Dict[str, str]): | ||||
|     def remove_cookies(self, cookies: List[Dict[str, str]]): | ||||
|         for cookie in cookies: | ||||
|             remove_cookie_by_name( | ||||
|                 self.cookie_jar, | ||||
| @@ -293,3 +253,7 @@ class Session(BaseConfigDict): | ||||
|     def auth(self, auth: dict): | ||||
|         assert {'type', 'raw_auth'} == auth.keys() | ||||
|         self['auth'] = auth | ||||
|  | ||||
|     @property | ||||
|     def is_anonymous(self): | ||||
|         return is_anonymous_session(self.session_id) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user