Skip to content

Commit ec1305f

Browse files
authored
Reject known problematic env vars (#308)
We expose all env vars by default to subprocesses to allow for customisation of package manager behaviour - such as custom indexes, authentication and requirements file env var interpolation. (An allow-list approach wouldn't work with all use-cases, plus wouldn't help the app at run-time.) To improve the error UX (particularly during initial buildpack bootstrap, where failures would otherwise look like a problem with the buildpack and not the user inputs), the buildpack now rejects known problematic env vars that may break the build / the app. The list of env vars was based on the env vars this buildpack sets, plus an audit of: - https://docs.python.org/3/using/cmdline.html#environment-variables - https://pip.pypa.io/en/stable/cli/pip/#general-options - https://pip.pypa.io/en/stable/cli/pip_install/#options This also unblocks #265. GUS-W-17454486.
1 parent b82a2a7 commit ec1305f

File tree

6 files changed

+97
-11
lines changed

6 files changed

+97
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
- The build now fails early if known problematic Python and pip-related env vars have been set by the user or earlier buildpacks. ([#308](https://github.com/heroku/buildpacks-python/pull/308))
1213
- The `PIP_PYTHON` env var is now only set at build time. ([#307](https://github.com/heroku/buildpacks-python/pull/307))
1314

1415
### Removed

src/checks.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use libcnb::Env;
2+
3+
// We expose all env vars by default to subprocesses to allow for customisation of package manager
4+
// behaviour (such as custom indexes, authentication and requirements file env var interpolation).
5+
// As such, we have to block known problematic env vars that may break the build / the app.
6+
// This list was based on the env vars this buildpack sets, plus an audit of:
7+
// https://docs.python.org/3/using/cmdline.html#environment-variables
8+
// https://pip.pypa.io/en/stable/cli/pip/#general-options
9+
// https://pip.pypa.io/en/stable/cli/pip_install/#options
10+
const FORBIDDEN_ENV_VARS: [&str; 12] = [
11+
"PIP_CACHE_DIR",
12+
"PIP_PREFIX",
13+
"PIP_PYTHON",
14+
"PIP_ROOT",
15+
"PIP_TARGET",
16+
"PIP_USER",
17+
"PYTHONHOME",
18+
"PYTHONINSPECT",
19+
"PYTHONNOUSERSITE",
20+
"PYTHONPLATLIBDIR",
21+
"PYTHONUSERBASE",
22+
"VIRTUAL_ENV",
23+
];
24+
25+
pub(crate) fn check_environment(env: &Env) -> Result<(), ChecksError> {
26+
if let Some(&name) = FORBIDDEN_ENV_VARS
27+
.iter()
28+
.find(|&name| env.contains_key(name))
29+
{
30+
return Err(ChecksError::ForbiddenEnvVar(name.to_string()));
31+
}
32+
33+
Ok(())
34+
}
35+
36+
/// Errors due to one of the environment checks failing.
37+
#[derive(Debug)]
38+
pub(crate) enum ChecksError {
39+
ForbiddenEnvVar(String),
40+
}

src/errors.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::checks::ChecksError;
12
use crate::django::DjangoCollectstaticError;
23
use crate::layers::pip::PipLayerError;
34
use crate::layers::pip_dependencies::PipDependenciesLayerError;
@@ -49,6 +50,7 @@ pub(crate) fn on_error(error: libcnb::Error<BuildpackError>) {
4950
fn on_buildpack_error(error: BuildpackError) {
5051
match error {
5152
BuildpackError::BuildpackDetection(error) => on_buildpack_detection_error(&error),
53+
BuildpackError::Checks(error) => on_buildpack_checks_error(error),
5254
BuildpackError::DeterminePackageManager(error) => on_determine_package_manager_error(error),
5355
BuildpackError::DjangoCollectstatic(error) => on_django_collectstatic_error(error),
5456
BuildpackError::DjangoDetection(error) => on_django_detection_error(&error),
@@ -70,6 +72,21 @@ fn on_buildpack_detection_error(error: &io::Error) {
7072
);
7173
}
7274

75+
fn on_buildpack_checks_error(error: ChecksError) {
76+
match error {
77+
ChecksError::ForbiddenEnvVar(name) => log_error(
78+
"Unsafe environment variable found",
79+
formatdoc! {"
80+
The environment variable '{name}' is set, however, it can
81+
cause problems with the build so we do not allow using it.
82+
83+
You must unset that environment variable. If you didn't set it
84+
yourself, check that it wasn't set by an earlier buildpack.
85+
"},
86+
),
87+
};
88+
}
89+
7390
fn on_determine_package_manager_error(error: DeterminePackageManagerError) {
7491
match error {
7592
DeterminePackageManagerError::CheckFileExists(io_error) => log_io_error(

src/main.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod checks;
12
mod detect;
23
mod django;
34
mod errors;
@@ -9,6 +10,7 @@ mod python_version_file;
910
mod runtime_txt;
1011
mod utils;
1112

13+
use crate::checks::ChecksError;
1214
use crate::django::DjangoCollectstaticError;
1315
use crate::layers::pip::PipLayerError;
1416
use crate::layers::pip_dependencies::PipDependenciesLayerError;
@@ -51,6 +53,15 @@ impl Buildpack for PythonBuildpack {
5153
}
5254

5355
fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
56+
// We inherit the current process's env vars, since we want `PATH` and `HOME` from the OS
57+
// to be set (so that later commands can find tools like Git in the base image), along
58+
// with previous-buildpack or user-provided env vars (so that features like env vars in
59+
// in requirements files work). We protect against broken user-provided env vars via the
60+
// checks feature and making sure that buildpack env vars take precedence in layers envs.
61+
let mut env = Env::from_current();
62+
63+
checks::check_environment(&env).map_err(BuildpackError::Checks)?;
64+
5465
// We perform all project analysis up front, so the build can fail early if the config is invalid.
5566
// TODO: Add a "Build config" header and list all config in one place?
5667
let package_manager = package_manager::determine_package_manager(&context.app_dir)
@@ -80,13 +91,6 @@ impl Buildpack for PythonBuildpack {
8091
)),
8192
}
8293

83-
// We inherit the current process's env vars, since we want `PATH` and `HOME` from the OS
84-
// to be set (so that later commands can find tools like Git in the base image), along
85-
// with previous-buildpack or user-provided env vars (so that features like env vars in
86-
// in requirements files work). We protect against broken user-provided env vars by
87-
// making sure that buildpack env vars take precedence in layers envs and command usage.
88-
let mut env = Env::from_current();
89-
9094
log_header("Installing Python");
9195
let python_layer_path = python::install_python(&context, &mut env, &python_version)?;
9296

@@ -126,6 +130,8 @@ impl Buildpack for PythonBuildpack {
126130
pub(crate) enum BuildpackError {
127131
/// I/O errors when performing buildpack detection.
128132
BuildpackDetection(io::Error),
133+
/// Errors due to one of the environment checks failing.
134+
Checks(ChecksError),
129135
/// Errors determining which Python package manager to use for a project.
130136
DeterminePackageManager(DeterminePackageManagerError),
131137
/// Errors running the Django collectstatic command.

tests/checks_test.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::tests::default_build_config;
2+
use indoc::indoc;
3+
use libcnb_test::{assert_contains, PackResult, TestRunner};
4+
5+
#[test]
6+
#[ignore = "integration test"]
7+
fn checks_reject_pythonhome_env_var() {
8+
let mut config = default_build_config("tests/fixtures/pyproject_toml_only");
9+
config.env("PYTHONHOME", "/invalid");
10+
config.expected_pack_result(PackResult::Failure);
11+
12+
TestRunner::default().build(config, |context| {
13+
assert_contains!(
14+
context.pack_stderr,
15+
indoc! {"
16+
[Error: Unsafe environment variable found]
17+
The environment variable 'PYTHONHOME' is set, however, it can
18+
cause problems with the build so we do not allow using it.
19+
20+
You must unset that environment variable. If you didn't set it
21+
yourself, check that it wasn't set by an earlier buildpack.
22+
"}
23+
);
24+
});
25+
}

tests/mod.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! These tests are not run via automatic integration test discovery, but instead are
44
//! imported in main.rs so that they have access to private APIs (see comment in main.rs).
55
6+
mod checks_test;
67
mod detect_test;
78
mod django_test;
89
mod package_manager_test;
@@ -38,13 +39,9 @@ fn default_build_config(fixture_path: impl AsRef<Path>) -> BuildConfig {
3839
("LD_LIBRARY_PATH", "/invalid"),
3940
("LIBRARY_PATH", "/invalid"),
4041
("PATH", "/invalid"),
41-
("PIP_CACHE_DIR", "/invalid"),
4242
("PIP_DISABLE_PIP_VERSION_CHECK", "0"),
4343
("PKG_CONFIG_PATH", "/invalid"),
44-
("PYTHONHOME", "/invalid"),
4544
("PYTHONPATH", "/invalid"),
46-
("PYTHONUSERBASE", "/invalid"),
47-
("VIRTUAL_ENV", "/invalid"),
4845
]);
4946

5047
config

0 commit comments

Comments
 (0)