DOC Check sha256 digests of tarballs in tutorial and examples before extraction
diff --git a/doc/tutorial/text_analytics/data/movie_reviews/fetch_data.py b/doc/tutorial/text_analytics/data/movie_reviews/fetch_data.py
index e591aca0f241b..67def14889774 100644
--- a/doc/tutorial/text_analytics/data/movie_reviews/fetch_data.py
+++ b/doc/tutorial/text_analytics/data/movie_reviews/fetch_data.py
@@ -1,27 +1,33 @@
 """Script to download the movie review dataset"""
 
-import os
+from pathlib import Path
+from hashlib import sha256
 import tarfile
-from contextlib import closing
 from urllib.request import urlopen
 
 
-URL = ("http://www.cs.cornell.edu/people/pabo/"
-       "movie-review-data/review_polarity.tar.gz")
+URL = "http://www.cs.cornell.edu/people/pabo/movie-review-data/review_polarity.tar.gz"
 
-ARCHIVE_NAME = URL.rsplit('/', 1)[1]
-DATA_FOLDER = "txt_sentoken"
+ARCHIVE_SHA256 = "fc0dccc2671af5db3c5d8f81f77a1ebfec953ecdd422334062df61ede36b2179"
+ARCHIVE_NAME = Path(URL.rsplit("/", 1)[1])
+DATA_FOLDER = Path("txt_sentoken")
 
 
-if not os.path.exists(DATA_FOLDER):
+if not DATA_FOLDER.exists():
 
-    if not os.path.exists(ARCHIVE_NAME):
+    if not ARCHIVE_NAME.exists():
         print("Downloading dataset from %s (3 MB)" % URL)
         opener = urlopen(URL)
-        with open(ARCHIVE_NAME, 'wb') as archive:
+        with open(ARCHIVE_NAME, "wb") as archive:
             archive.write(opener.read())
 
-    print("Decompressing %s" % ARCHIVE_NAME)
-    with closing(tarfile.open(ARCHIVE_NAME, "r:gz")) as archive:
-        archive.extractall(path='.')
-    os.remove(ARCHIVE_NAME)
+    try:
+        print("Checking the integrity of the archive")
+        assert sha256(ARCHIVE_NAME.read_bytes()).hexdigest() == ARCHIVE_SHA256
+
+        print("Decompressing %s" % ARCHIVE_NAME)
+        with tarfile.open(ARCHIVE_NAME, "r:gz") as archive:
+            archive.extractall(path=".")
+
+    finally:
+        ARCHIVE_NAME.unlink()
diff --git a/doc/tutorial/text_analytics/data/twenty_newsgroups/fetch_data.py b/doc/tutorial/text_analytics/data/twenty_newsgroups/fetch_data.py
deleted file mode 100644
index 983989d8c0e25..0000000000000
--- a/doc/tutorial/text_analytics/data/twenty_newsgroups/fetch_data.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Script to download the 20 newsgroups text classification set"""
-
-import os
-import tarfile
-from contextlib import closing
-from urllib.request import urlopen
-
-URL = ("http://people.csail.mit.edu/jrennie/"
-       "20Newsgroups/20news-bydate.tar.gz")
-
-ARCHIVE_NAME = URL.rsplit('/', 1)[1]
-TRAIN_FOLDER = "20news-bydate-train"
-TEST_FOLDER = "20news-bydate-test"
-
-
-if not os.path.exists(TRAIN_FOLDER) or not os.path.exists(TEST_FOLDER):
-
-    if not os.path.exists(ARCHIVE_NAME):
-        print("Downloading dataset from %s (14 MB)" % URL)
-        opener = urlopen(URL)
-        with open(ARCHIVE_NAME, 'wb') as archive:
-            archive.write(opener.read())
-
-    print("Decompressing %s" % ARCHIVE_NAME)
-    with closing(tarfile.open(ARCHIVE_NAME, "r:gz")) as archive:
-        archive.extractall(path='.')
-    os.remove(ARCHIVE_NAME)
diff --git a/doc/whats_new/v1.2.rst b/doc/whats_new/v1.2.rst
index b6458339fa675..1c645121d26ec 100644
--- a/doc/whats_new/v1.2.rst
+++ b/doc/whats_new/v1.2.rst
@@ -84,6 +84,10 @@ Changes impacting all modules
   :pr:`23604` and :pr:`23585` by :user:`Julien Jerphanion <jjerphan>`,
   :user:`Olivier Grisel <ogrisel>`, and `Thomas Fan`_.
 
+- |Fix| Systematically check the sha256 digest of dataset tarballs used in code
+  examples in the documentation.
+  :pr:`24617` by :user:`Olivier Grisel <ogrisel>` and `Thomas Fan`_. Thanks to
+  `Sim4n6 <https://huntr.dev/users/sim4n6>`_ for the report.
 
 Changelog
 ---------
diff --git a/examples/applications/plot_out_of_core_classification.py b/examples/applications/plot_out_of_core_classification.py
index a8e4f9b72a3b0..721ba0159e692 100644
--- a/examples/applications/plot_out_of_core_classification.py
+++ b/examples/applications/plot_out_of_core_classification.py
@@ -18,9 +18,9 @@
 #          @FedericoV <https://github.com/FedericoV/>
 # License: BSD 3 clause
 
-from glob import glob
 import itertools
-import os.path
+from pathlib import Path
+from hashlib import sha256
 import re
 import tarfile
 import time
@@ -149,14 +149,17 @@ def stream_reuters_documents(data_path=None):
         "http://archive.ics.uci.edu/ml/machine-learning-databases/"
         "reuters21578-mld/reuters21578.tar.gz"
     )
+    ARCHIVE_SHA256 = "3bae43c9b14e387f76a61b6d82bf98a4fb5d3ef99ef7e7075ff2ccbcf59f9d30"
     ARCHIVE_FILENAME = "reuters21578.tar.gz"
 
     if data_path is None:
-        data_path = os.path.join(get_data_home(), "reuters")
-    if not os.path.exists(data_path):
+        data_path = Path(get_data_home()) / "reuters"
+    else:
+        data_path = Path(data_path)
+    if not data_path.exists():
         """Download the dataset."""
         print("downloading dataset (once and for all) into %s" % data_path)
-        os.mkdir(data_path)
+        data_path.mkdir(parents=True, exist_ok=True)
 
         def progress(blocknum, bs, size):
             total_sz_mb = "%.2f MB" % (size / 1e6)
@@ -164,16 +167,21 @@ def progress(blocknum, bs, size):
             if _not_in_sphinx():
                 sys.stdout.write("\rdownloaded %s / %s" % (current_sz_mb, total_sz_mb))
 
-        archive_path = os.path.join(data_path, ARCHIVE_FILENAME)
+        archive_path = data_path / ARCHIVE_FILENAME
+
         urlretrieve(DOWNLOAD_URL, filename=archive_path, reporthook=progress)
         if _not_in_sphinx():
             sys.stdout.write("\r")
+
+        # Check that the archive was not tampered:
+        assert sha256(archive_path.read_bytes()).hexdigest() == ARCHIVE_SHA256
+
         print("untarring Reuters dataset...")
         tarfile.open(archive_path, "r:gz").extractall(data_path)
         print("done.")
 
     parser = ReutersParser()
-    for filename in glob(os.path.join(data_path, "*.sgm")):
+    for filename in data_path.glob("*.sgm"):
         for doc in parser.parse(open(filename, "rb")):
             yield doc
 

Archives with previously lacking sha256 checks:

- <del>`20Newsgroups/20news-bydate.tar.gz`</del> (update: `fetch_data.py` script deleted since actually unused)
- `reuters21578-mld/reuters21578.tar.gz`
- `movie-review-data/review_polarity.tar.gz`

This is a security concern not to check the digests of tarballs before extracting them because they could overwrite sensitive system files such as `/etc/hosts`.

Note that this PR only fixes code in documentation (tutorial and examples) and not library code. Our datasets fetchers under the `sklearn` namespace 2s already checking the digests systematically. Still fixing those code snippets in the documentation is good from an education point of view.

The scikit-learn-1.1.2.tar.gz source tarball contains those code snippets because it includes the documentation and example files. But this is not the case for the wheel files and conda packages. Not sure if this warrants a security bugfix release or not.