Skip to content

Commit 2305eb3

Browse files
claudespoorcc
authored andcommitted
Warn on plaintext transport scheme in project URLs (DFT-01)
Add `_warn_if_plaintext()` to `dfetch/manifest/project.py` that emits a WARNING-level log message when a project URL uses `http://`, `git://`, or `svn://`. The check fires in `ProjectEntry.__init__` (direct `url:` field) and in `ProjectEntry.set_remote()` (remote-resolved URLs), covering both manifest authoring paths. This partially closes the open gap in threat DFT-01 ("Unencrypted transport interception") by surfacing the risk at configuration-validation time rather than silently fetching over plaintext. Users are directed to HTTPS, svn+https://, or SSH. https://claude.ai/code/session_01NAFVNoL8K8itC3KRuC4wxV
1 parent 8ee5ea6 commit 2305eb3

8 files changed

Lines changed: 123 additions & 43 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Release 0.14.0 (unreleased)
22
===========================
33

4+
* Warn when a project URL uses a plaintext transport scheme (#1229)
45
* Documentation and threat-model clarifications for existing release attestation support (#1208)
56
* Report SVN externals fetched during update (#1220)
67
* Use ``.cdx.json`` as the default extension for CycloneDX SBOM reports (#1118)

dfetch/manifest/project.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,13 +321,38 @@
321321
import copy
322322
from collections.abc import Sequence
323323
from dataclasses import dataclass, field
324+
from urllib.parse import urlsplit, urlunsplit
324325

325326
from typing_extensions import Required, TypedDict
326327

328+
from dfetch.log import get_logger
327329
from dfetch.manifest.remote import Remote
328330
from dfetch.manifest.version import Version
329331
from dfetch.util.util import always_str_list, str_if_possible
330332

333+
logger = get_logger(__name__)
334+
335+
_PLAINTEXT_SCHEMES = frozenset({"http", "git", "svn"})
336+
337+
338+
def plaintext_warning(url: str) -> str:
339+
"""Return a warning string if *url* uses a plaintext transport, else empty string."""
340+
parsed = urlsplit(url)
341+
scheme = parsed.scheme.lower()
342+
if scheme not in _PLAINTEXT_SCHEMES:
343+
return ""
344+
host = parsed.hostname or ""
345+
try:
346+
port = parsed.port
347+
except ValueError:
348+
port = None
349+
netloc = f"{host}:{port}" if isinstance(port, int) else host
350+
redacted_url = urlunsplit((scheme, netloc, parsed.path, "", ""))
351+
return (
352+
f"Project URL '{redacted_url}' uses plaintext transport ({scheme}://). "
353+
"Use https:// or SSH (e.g. svn+ssh://) to prevent interception."
354+
)
355+
331356

332357
@dataclass
333358
class Integrity:

dfetch/project/subproject.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import Callable, Sequence
77

88
from dfetch.log import get_logger
9-
from dfetch.manifest.project import ProjectEntry
9+
from dfetch.manifest.project import ProjectEntry, plaintext_warning
1010
from dfetch.manifest.version import Version
1111
from dfetch.project.abstract_check_reporter import AbstractCheckReporter
1212
from dfetch.project.metadata import Dependency, InvalidMetadataError, Metadata
@@ -129,6 +129,8 @@ def update(
129129
f"Fetching {to_fetch}",
130130
enabled=self._show_animations,
131131
):
132+
if warning := plaintext_warning(self.__project.remote_url):
133+
logger.print_warning_line(self.__project.name, warning)
132134
actually_fetched, dependency = self._fetch_impl(to_fetch)
133135
self._log_project(f"Fetched {actually_fetched}")
134136

@@ -213,6 +215,8 @@ def check_for_update(
213215
with logger.status(
214216
self.__project.name, "Checking", enabled=self._show_animations
215217
):
218+
if warning := plaintext_warning(self.__project.remote_url):
219+
logger.print_warning_line(self.__project.name, warning)
216220
latest_version = self._check_for_newer_version()
217221

218222
if not latest_version:

doc/explanation/security.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ to reproduce a deterministic dependency state.
9696
allow exfiltration risks if upstream sources are compromised or intentionally
9797
malicious.
9898

99+
.. note::
100+
101+
dfetch warns at manifest-load time when a project URL uses a plaintext
102+
transport scheme (``http://``, ``git://``, or ``svn://``). Use ``https://``
103+
or SSH (e.g. ``svn+ssh://``) to protect dependency fetches against interception.
104+
99105
Threat Models
100106
-------------
101107

doc/explanation/threat_model_supply_chain.rst

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -546,22 +546,22 @@ Dataflows
546546
* - DF-22: PR enters code review
547547
- A-01b: GitHub Repository (feature branches / PRs)
548548
- A-04: Release Gate / Code Review
549-
-
549+
-
550550

551551
* - DF-12: Main branch workflows drive CI execution
552552
- A-01: GitHub Repository (main / protected)
553553
- A-06: GitHub Actions Workflow
554-
-
554+
-
555555

556556
* - DF-13a: PR CI checkout
557557
- A-01b: GitHub Repository (feature branches / PRs)
558558
- A-02: GitHub Actions Infrastructure
559-
-
559+
-
560560

561561
* - DF-13b: Release CI checkout
562562
- A-01: GitHub Repository (main / protected)
563563
- A-02: GitHub Actions Infrastructure
564-
-
564+
-
565565

566566
* - DF-14: CI cache restore
567567
- A-08b: GitHub Actions Build Cache
@@ -571,12 +571,12 @@ Dataflows
571571
* - DF-15: Workflow triggers build step
572572
- A-06: GitHub Actions Workflow
573573
- A-08: Python Build (wheel / sdist)
574-
-
574+
-
575575

576576
* - DF-15b: Built wheel/sdist artifacts
577577
- A-08: Python Build (wheel / sdist)
578578
- A-02: GitHub Actions Infrastructure
579-
-
579+
-
580580

581581
* - DF-16: CI fetches build/dev deps from PyPI
582582
- A-03: PyPI / TestPyPI
@@ -586,7 +586,7 @@ Dataflows
586586
* - DF-17: Build tools consumed by build step
587587
- A-07: dfetch Build / Dev Dependencies
588588
- A-08: Python Build (wheel / sdist)
589-
-
589+
-
590590

591591
* - DF-18: CI cache write
592592
- A-02: GitHub Actions Infrastructure
@@ -601,7 +601,7 @@ Dataflows
601601
* - DF-23: Approved merge to main
602602
- A-04: Release Gate / Code Review
603603
- A-01: GitHub Repository (main / protected)
604-
-
604+
-
605605

606606
* - DF-24: Publish wheel to PyPI (OIDC)
607607
- A-02: GitHub Actions Infrastructure
@@ -1022,5 +1022,3 @@ Controls
10221022
- Test result attestation on source archive
10231023
- DFT-31
10241024
- The CI test workflow (``test.yml``) generates an in-toto test result attestation (predicate type ``https://in-toto.io/attestation/test-result/v0.1``) for every release and main-branch commit. The attestation proves the full CI test suite ran against the exact source archive and every check passed, before any binary was produced from that source. Consumers can verify it using ``gh attestation verify dfetch-source.tar.gz`` with ``--predicate-type https://in-toto.io/attestation/test-result/v0.1`` and ``--cert-identity`` pinned to ``test.yml`` at the release tag ref. This provides an additional layer of assurance beyond build provenance: not only was the artifact produced from the official commit, but the test suite demonstrably passed on that exact source before any binary was built. ``.github/workflows/test.yml``
1025-
1026-

doc/explanation/threat_model_usage.rst

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -749,12 +749,12 @@ Dataflows
749749
* - DF-01: Invoke dfetch command
750750
- Developer
751751
- A-22: dfetch Process
752-
-
752+
-
753753

754754
* - DF-02: Read manifest
755755
- A-12: dfetch Manifest
756756
- A-22: dfetch Process
757-
-
757+
-
758758

759759
* - DF-03a: Fetch VCS content - HTTPS/SSH
760760
- A-22: dfetch Process
@@ -799,102 +799,102 @@ Dataflows
799799
* - DF-07: Write vendored files
800800
- A-22: dfetch Process
801801
- A-13: Fetched Source Code
802-
-
802+
-
803803

804804
* - DF-08: Write dependency metadata
805805
- A-22: dfetch Process
806806
- A-18: Dependency Metadata
807-
-
807+
-
808808

809809
* - DF-09: Write SBOM
810810
- A-22: dfetch Process
811811
- A-15: SBOM Output (CycloneDX)
812-
-
812+
-
813813

814814
* - DF-16: Read dependency metadata
815815
- A-18: Dependency Metadata
816816
- A-22: dfetch Process
817-
-
817+
-
818818

819819
* - DF-10: Read patch for application
820820
- A-19: Patch Files
821821
- A-25: Patch Application (patch-ng)
822-
-
822+
-
823823

824824
* - DF-10b: Write patched files to vendor directory
825825
- A-25: Patch Application (patch-ng)
826826
- A-13: Fetched Source Code
827-
-
827+
-
828828

829829
* - DF-15: Vendored source to build
830830
- A-13: Fetched Source Code
831831
- A-11: Consumer Build System
832-
-
832+
-
833833

834834
* - DF-11: Dispatch archive bytes to extraction
835835
- A-22: dfetch Process
836836
- A-24: Archive Extraction (tarfile / zipfile)
837-
-
837+
-
838838

839839
* - DF-12: Write extracted archive to temp dir
840840
- A-24: Archive Extraction (tarfile / zipfile)
841841
- A-20: Local VCS Cache (temp)
842-
-
842+
-
843843

844844
* - DF-13: Dispatch SVN export subprocess
845845
- A-22: dfetch Process
846846
- A-26: SVN Export (svn export)
847-
-
847+
-
848848

849849
* - DF-14: Write SVN export to temp dir
850850
- A-26: SVN Export (svn export)
851851
- A-20: Local VCS Cache (temp)
852-
-
852+
-
853853

854854
* - DF-23: Dispatch git clone subprocess
855855
- A-22: dfetch Process
856856
- A-27: Git Clone (git init / fetch / checkout)
857-
-
857+
-
858858

859859
* - DF-24: Write git checkout to temp dir
860860
- A-27: Git Clone (git init / fetch / checkout)
861861
- A-20: Local VCS Cache (temp)
862-
-
862+
-
863863

864864
* - DF-17: Write audit / check reports
865865
- A-22: dfetch Process
866866
- A-21: Audit / Check Reports
867-
-
867+
-
868868

869869
* - DF-22: Read validated content from local VCS cache
870870
- A-20: Local VCS Cache (temp)
871871
- A-22: dfetch Process
872-
-
872+
-
873873

874874
* - DF-18: Read integrity hash for archive verification
875875
- A-12: dfetch Manifest
876876
- A-22: dfetch Process
877-
-
877+
-
878878

879879
* - DF-18b: Write computed hash to manifest (dfetch freeze)
880880
- A-22: dfetch Process
881881
- A-12: dfetch Manifest
882-
-
882+
-
883883

884884
* - DF-20: Author / maintain dfetch.yaml
885885
- Developer
886886
- A-12: dfetch Manifest
887-
-
887+
-
888888

889889
* - DF-19: VCS server publishes source attestation (not consumed by dfetch)
890890
- A-09: Remote VCS Server
891891
- A-23: Upstream Source Attestation (VSA)
892-
-
892+
-
893893

894894
* - DF-21: Create / maintain patch files
895895
- Developer
896896
- A-19: Patch Files
897-
-
897+
-
898898

899899

900900
Threats
@@ -1712,5 +1712,3 @@ Controls
17121712
- Hash algorithm allowlist (SHA-256/384/512 only)
17131713
- DFT-30
17141714
- ``integrity_hash.py`` accepts only ``sha256:``, ``sha384:``, and ``sha512:`` prefixes; any other algorithm prefix is rejected at parse time. MD5 and SHA-1 are not accepted. This directly mitigates DFT-30 (SLSA M2: exploit cryptographic hash collisions) by ensuring that integrity hashes, when present, use algorithms with no known practical collision attacks. ``dfetch/vcs/integrity_hash.py``
1715-
1716-

security/tm_usage.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def _make_usage_vcs_dataflows(
370370
"``git fetch`` over ``http://`` or SVN over ``svn://`` (plain, non-TLS). "
371371
"dfetch accepts these protocols without enforcement - no TLS check in manifest "
372372
"schema. Traffic is unencrypted; MITM can substitute repository content. "
373-
"RECOMMENDATION: restrict manifest URLs to HTTPS / svn+https:// / SSH."
373+
"RECOMMENDATION: restrict manifest URLs to ``https://`` or ``svn+ssh://``."
374374
)
375375
df03_plain.protocol = "HTTP / SVN"
376376
df03_plain.controls.isEncrypted = False
@@ -958,6 +958,24 @@ def build_model() -> TM:
958958
"rejects control characters in all string fields."
959959
),
960960
),
961+
Control(
962+
id="C-009",
963+
name="Plaintext transport detection",
964+
assets=["A-09", "A-22"],
965+
threats=["DFT-26"],
966+
reference="dfetch/manifest/project.py, dfetch/project/subproject.py",
967+
description=(
968+
"``plaintext_warning()`` (``dfetch/manifest/project.py``) inspects the "
969+
"resolved remote URL immediately before each VCS command is issued "
970+
"(inside the ``check_for_update`` and ``update`` spinners in "
971+
"``subproject.py``). If the scheme is ``http://``, ``git://``, or "
972+
"``svn://``, a visible warning is emitted naming the redacted URL "
973+
"(credentials stripped from the userinfo component) and recommending "
974+
"``https://`` or ``svn+ssh://``. "
975+
"Detection only — dfetch still proceeds with the plaintext connection; "
976+
"the control raises user awareness but does not enforce scheme selection."
977+
),
978+
),
961979
Control(
962980
id="C-034",
963981
name="Hash algorithm allowlist (SHA-256/384/512 only)",
@@ -1291,15 +1309,15 @@ def build_model() -> TM:
12911309
),
12921310
ThreatResponse(
12931311
"DFT-26",
1294-
"accept",
1312+
"mitigate",
12951313
risk="High",
12961314
stride=["Tampering", "Information Disclosure"],
12971315
note=(
1298-
"dfetch accepts ``http://``, ``svn://``, and other non-TLS scheme URLs; "
1299-
"HTTPS enforcement is the manifest author's responsibility. "
1300-
"Accepted based on the **No HTTPS enforcement** assumption: HTTPS enforcement "
1301-
"is the responsibility of the manifest author; dfetch accepts non-TLS scheme "
1302-
"URLs as written and does not upgrade or reject them."
1316+
"C-009 emits a visible warning immediately before the VCS command when a "
1317+
"plaintext scheme (``http://``, ``git://``, ``svn://``) is detected, "
1318+
"with credentials redacted and ``https://`` / ``svn+ssh://`` recommended. "
1319+
"Detection only — dfetch does not reject or upgrade plaintext URLs; "
1320+
"scheme selection remains the manifest author's responsibility."
13031321
),
13041322
),
13051323
ThreatResponse(

tests/test_manifest.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
RequestedProjectNotFoundError,
1919
)
2020
from dfetch.manifest.parse import find_manifest, get_submanifests
21-
from dfetch.manifest.project import ProjectEntry, ProjectEntryDict
21+
from dfetch.manifest.project import ProjectEntry, ProjectEntryDict, plaintext_warning
2222
from dfetch.manifest.remote import Remote
2323

2424
BASIC_MANIFEST = """
@@ -406,6 +406,36 @@ def test_remove_last_project_updates_manifest_with_empty_list() -> None:
406406
assert not manifest.projects
407407

408408

409+
# ---------------------------------------------------------------------------
410+
# Plaintext URL transport warnings (DFT-01)
411+
# ---------------------------------------------------------------------------
412+
413+
414+
@pytest.mark.parametrize(
415+
"plaintext_url",
416+
[
417+
"http://git.example.com/org/repo",
418+
"git://git.example.com/org/repo",
419+
"svn://svn.example.com/repo/trunk",
420+
],
421+
)
422+
def test_plaintext_warning_for_plaintext_scheme(plaintext_url: str) -> None:
423+
"""plaintext_warning returns a non-empty message for plaintext schemes."""
424+
assert "plaintext transport" in plaintext_warning(plaintext_url)
425+
426+
427+
def test_plaintext_warning_empty_for_https() -> None:
428+
"""plaintext_warning returns empty string for HTTPS URLs."""
429+
assert plaintext_warning("https://git.example.com/org/repo") == ""
430+
431+
432+
def test_plaintext_warning_redacts_credentials() -> None:
433+
"""Credentials embedded in a plaintext URL are not exposed in the warning."""
434+
warning = plaintext_warning("http://user:secret@git.example.com/repo")
435+
assert "plaintext transport" in warning
436+
assert "secret" not in warning
437+
438+
409439
# ---------------------------------------------------------------------------
410440
# Empty manifest (no projects key)
411441
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)