8000 fix(security): reject option-like package names in adapters; constant-time API key check by devin-ai-integration[bot] · Pull Request #107 · dominicusin/package-maximizer · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions package_maximizer/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@

from ..core.package import Package

_SAFE_NAME_RE = re.compile(r"^[^\s-]\S{0,255}$")


def _is_safe_package_name(name: str) -> bool:
"""Reject names that a CLI could parse as options (e.g. ``--registry=...``)."""
return isinstance(name, str) and bool(_SAFE_NAME_RE.match(name))


@dataclass
class PackageMetadata:
Expand Down Expand Up @@ -59,6 +66,8 @@ class APTMetadataAdapter:

def fetch(self, package_name: str) -> PackageMetadata | None:
"""Получить метаданные через apt-cache show."""
if not _is_safe_package_name(package_name):
return None
try:
result = subprocess.run(
["apt-cache", "show", package_name],
Expand Down Expand Up @@ -177,6 +186,8 @@ class PipMetadataAdapter:

def fetch(self, package_name: str) -> PackageMetadata | None:
"""Получить метаданные через pip show."""
if not _is_safe_package_name(package_name):
return None
try:
result = subprocess.run(
["pip", "show", package_name],
Expand Down Expand Up @@ -267,6 +278,8 @@ class PacmanMetadataAdapter:

def fetch(self, package_name: str) -> PackageMetadata | None:
"""Получить метаданные через pacman -Si."""
if not _is_safe_package_name(package_name):
return None
try:
result = subprocess.run(
["pacman", "-Si", package_name],
Expand Down Expand Up @@ -350,6 +363,8 @@ class NpmMetadataAdapter:

def fetch(self, package_name: str) -> PackageMetadata | None:
"""Получить метаданные через npm view."""
if not _is_safe_package_name(package_name):
return None
try:
result = subprocess.run(
["npm", "view", package_name, "json"],
Expand Down Expand Up @@ -453,6 +468,8 @@ class BrewMetadataAdapter:

def fetch(self, package_name: str) -> PackageMetadata | None:
"""Получить метаданные через brew info --json=v2."""
if not _is_safe_package_name(package_name):
return None
try:
result = subprocess.run(
["brew", "info", "--json=v2", package_name],
Expand Down
6 changes: 3 additions & 3 deletions package_maximizer/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,13 +1105,13 @@ def analyze(packages, manager, solver, metadata, conflicts, depends, output):
click.echo(f"\nSelected packages: {', '.join(result)}")

if excluded:
click.echo(f"\n--- Exclusion Reasons ---")
click.echo("\n--- Exclusion Reasons ---")
for name, reasons in conflict_map.items():
if reasons:
click.echo(f" {name}: {'; '.join(reasons)}")

if bottlenecks:
click.echo(f"\n--- Bottleneck Packages (most conflicts) ---")
click.echo("\n--- Bottleneck Packages (most conflicts) ---")
for b, c in bottlenecks:
click.echo(f" {b}: {c} conflicts")

Expand Down Expand Up @@ -1154,7 +1154,7 @@ def compare(packages, manager, metadata, output):
manager = cfg.default_manager if cfg else "apt"

try:
manager_enum = PackageManagerType(manager)
PackageManagerType(manager)
except ValueError:
click.echo(f"Error: Unknown package manager '{manager}'", err=True)
sys.exit(1)
Expand Down
2 changes: 1 addition & 1 deletion package_maximizer/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import json
import logging
import os
from dataclasses import dataclass, field
from dataclasses import dataclass
from pathlib import Path
from typing import Any

Expand Down
3 changes: 1 addition & 2 deletions package_maximizer/core/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any
from dataclasses import dataclass


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion package_maximizer/di/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from __future__ import annotations

import inspect
from typing import Any, Callable, Generic, TypeVar
from typing import Any, Callable, TypeVar

T = TypeVar("T")

Expand Down
2 changes: 0 additions & 2 deletions package_maximizer/integrations/real_repo_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

import logging
import subprocess
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

from ..core.package import Package
Expand Down
1 change: 0 additions & 1 deletion package_maximizer/parsers/brew_parser.py
10BC8
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from __future__ import annotations

import logging
import re
import subprocess
from typing import TYPE_CHECKING

Expand Down
2 changes: 0 additions & 2 deletions package_maximizer/parsers/dnf_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from __future__ import annotations

import logging
import re
import subprocess
from typing import TYPE_CHECKING

from ..core.interfaces import PackageParser
Expand Down
2 changes: 0 additions & 2 deletions package_maximizer/parsers/pacman_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from __future__ import annotations

import logging
import re
import subprocess
from typing import TYPE_CHECKING

from ..core.interfaces import PackageParser
Expand Down
4 changes: 1 addition & 3 deletions package_maximizer/solvers/maxsat_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import TYPE_CHECKING, Iterable

from ..core.interfaces import ConstraintSolver
from ..core.model_encoder import ModelConstraints, encode_packages
from ..core.model_encoder import encode_packages

if TYPE_CHECKING:
from ..core.package import Package
Expand Down Expand Up @@ -115,8 +115,6 @@ def solve_with_weights(
)

constraints = encode_packages(package_list)
var_map = {name: i + 1 for i, name in enumerate(constraints.packages)}

selected: list[str] = []
selected_names: set[str] = set()

Expand Down
3 changes: 1 addition & 2 deletions package_maximizer/solvers/minisat_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import TYPE_CHECKING, Iterable

from ..core.interfaces import ConstraintSolver
from ..core.model_encoder import ModelConstraints, encode_packages
from ..core.model_encoder import encode_packages

if TYPE_CHECKING:
from ..core.package import Package
Expand Down Expand Up @@ -113,7 +113,6 @@ def solve_with_weights(
)

constraints = encode_packages(package_list)
var_map = {name: i + 1 for i, name in enumerate(constraints.packages)}

selected = []
selected_names = set()
Expand Down
2 changes: 1 addition & 1 deletion package_maximizer/solvers/ortools_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import TYPE_CHECKING, Iterable

from ..core.interfaces import ConstraintSolver
from ..core.model_encoder import ModelConstraints, encode_packages
from ..core.model_encoder import encode_packages

if TYPE_CHECKING:
from ..core.package import Package
Expand Down
2 changes: 1 addition & 1 deletion package_maximizer/solvers/pulp_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import TYPE_CHECKING, Iterable, Optional

from ..core.interfaces import ConstraintSolver
from ..core.model_encoder import ModelConstraints, encode_packages
from ..core.model_encoder import encode_packages

if TYPE_CHECKING:
from ..core.package import Package
Expand Down
4 changes: 2 additions & 2 deletions package_maximizer/solvers/z3_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from typing import TYPE_CHECKING

from ..core.interfaces import ConstraintSolver
from ..core.model_encoder import ModelConstraints, encode_packages
from ..core.model_encoder import encode_packages

if TYPE_CHECKING:
from ..core.package import Package
pass

logger = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions package_maximizer/utils/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
import time
from dataclasses import dataclass, field
from typing import Any, Callable, TypeVar
from typing import Any, TypeVar

from ..core.package import Package
from ..solvers import SOLVER_REGISTRY
Expand Down Expand Up @@ -267,7 +267,7 @@ def print_report(self, report: BenchmarkReport) -> None:
summary = report.get_summary()

print(f"\n{'='*70}")
print(f"Benchmark Report")
print("Benchmark Report")
print(f"{'='*70}")
print(f"Total solvers: {summary['total_solvers']}")
print(f"Best solver: {summary['best_solver']} ({summary['best_time']:.4f}s)")
Expand Down
1 change: 0 additions & 1 deletion package_maximizer/utils/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import hashlib
import json
import logging
import os
import time
from pathlib import Path
from typing import Any, Callable, TypeVar
Expand Down
5 changes: 3 additions & 2 deletions package_maximizer/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import hmac
import os
import time
from functools import wraps
Expand Down Expand Up @@ -137,7 +138,7 @@ def decorated(*args, **kwargs):

# Check API key
api_key = request.headers.get("X-API-Key") or request.args.get("api_key")
if not api_key or api_key != API_KEY:
if not api_key or not hmac.compare_digest(api_key, API_KEY):
return (
jsonify(
{
Expand Down Expand Up @@ -951,7 +952,7 @@ def compare_post() -> tuple[dict, int]:
metadata = data.get("metadata", False)

try:
manager_enum = PackageManagerType(manager)
PackageManagerType(manager)
except ValueError:
valid = [m.value for m in PackageManagerType]
return (
Expand Down
28 changes: 28 additions & 0 deletions tests/test_audit_fixes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Regression tests for audit findings."""

from unittest.mock import patch

import pytest

from package_maximizer.adapters import _is_safe_package_name, get_adapter


@pytest.mark.parametrize("manager", ["apt", "pip", "pacman", "npm", "brew"])
@pytest.mark.parametrize(
"name", ["--registry=http://evil.example", "-h", "foo bar", "", "a\nb"]
)
def test_adapters_refuse_option_like_names(manager, name):
with patch("package_maximizer.adapters.subprocess.run") as run:
assert get_adapter(manager).fetch(name) is None
run.assert_not_called()


@pytest.mark.parametrize(
"name", ["curl", "@types/node", "python3.11-numpy", "lib++", "g++-12", "x" * 256]
)
def test_safe_names_accepted(name):
assert _is_safe_package_name(name)


def test_overlong_name_rejected():
assert not _is_safe_package_name("x" * 257)
Loading
0