File mercurial-CVE-2025-2361.patch of Package mercurial.38038

exporting patch:
# HG changeset patch
# User Raphaël Gomès <rgomes@octobus.net>
# Date 1742340720 -3600
#      Wed Mar 19 00:32:00 2025 +0100
# Branch stable
# Node ID a5c72ed2929341d97b11968211c880854803f003
# Parent  74439d1cbebaa9ff8f8300e37e93b42e6d381be4
hgweb: fix XSS vulnerability in hgweb (CVE-2025-2361)

818598f5bc8b91 is the change that introduced the vulnerability (in 2006!)
that was disclosed to us, but I found a similar pattern in other places
in the code.

Since XSS escaping is actually hard and that would mean vendoring some
better sanitation tool, I decided to simply remove user input from any
HTML output in hgweb, hopefully in all places.

---
 mercurial/hgweb/hgweb_mod.py    |    5 ++-
 mercurial/hgweb/webcommands.py  |   21 ++++++++++-----
 tests/test-archive.t            |   54 ++++++++++++++++++++--------------------
 tests/test-hgweb.t              |   10 +++----
 tests/test-lfs-serve-access.t   |    4 +-
 tests/test-remotefilelog-http.t |    4 +-
 6 files changed, 54 insertions(+), 44 deletions(-)

Index: mercurial-5.9.1/mercurial/hgweb/hgweb_mod.py
===================================================================
--- mercurial-5.9.1.orig/mercurial/hgweb/hgweb_mod.py	2021-08-30 20:10:34.000000000 +0200
+++ mercurial-5.9.1/mercurial/hgweb/hgweb_mod.py	2025-03-20 19:58:46.757566864 +0100
@@ -467,7 +467,10 @@
                 res.headers[b'ETag'] = tag
 
             if cmd not in webcommands.__all__:
-                msg = b'no such method: %s' % cmd
+                msg = b'method not found'
+                # /!\ Do not print `cmd` here unless you do *extensive*
+                # escaping.
+                # Because XSS escaping is hard, we just don't risk it.
                 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
             else:
                 # Set some globals appropriate for web handlers. Commands can
Index: mercurial-5.9.1/mercurial/hgweb/webcommands.py
===================================================================
--- mercurial-5.9.1.orig/mercurial/hgweb/webcommands.py	2021-08-30 20:10:34.000000000 +0200
+++ mercurial-5.9.1/mercurial/hgweb/webcommands.py	2025-03-20 19:57:58.385801249 +0100
@@ -585,7 +585,9 @@
             h[None] = None  # denotes files present
 
     if mf and not files and not dirs:
-        raise ErrorResponse(HTTP_NOT_FOUND, b'path not found: ' + path)
+        # /!\ Do not print `path` here unless you do *extensive* escaping.
+        # Because XSS escaping is hard, we just don't risk it.
+        raise ErrorResponse(HTTP_NOT_FOUND, b'path not found')
 
     def filelist(context):
         for f in sorted(files):
@@ -1253,11 +1255,15 @@
     key = web.req.qsparams[b'node']
 
     if type_ not in webutil.archivespecs:
-        msg = b'Unsupported archive type: %s' % stringutil.pprint(type_)
+        # /!\ Do not print `type_` here unless you do *extensive* escaping.
+        # Because XSS escaping is hard, we just don't risk it.
+        msg = b'Unsupported archive type'
         raise ErrorResponse(HTTP_NOT_FOUND, msg)
 
-    if not ((type_ in allowed or web.configbool(b"web", b"allow" + type_))):
-        msg = b'Archive type not allowed: %s' % type_
+    if not (type_ in allowed or web.configbool(b"web", b"allow" + type_)):
+        # /!\ Do not print `type_` here unless you do *extensive* escaping.
+        # Because XSS escaping is hard, we just don't risk it.
+        msg = b'Archive type not allowed'
         raise ErrorResponse(HTTP_FORBIDDEN, msg)
 
     reponame = re.sub(br"\W+", b"-", os.path.basename(web.reponame))
@@ -1276,9 +1282,10 @@
         if pats:
             files = [f for f in ctx.manifest().keys() if match(f)]
             if not files:
-                raise ErrorResponse(
-                    HTTP_NOT_FOUND, b'file(s) not found: %s' % file
-                )
+                # /!\ Do not print `files` here unless you do *extensive*
+                # escaping.
+                # Because XSS escaping is hard, we just don't risk it.
+                raise ErrorResponse(HTTP_NOT_FOUND, b'file(s) not found')
 
     mimetype, artype, extension, encoding = webutil.archivespecs[type_]
 
Index: mercurial-5.9.1/tests/test-archive.t
===================================================================
--- mercurial-5.9.1.orig/tests/test-archive.t	2021-08-30 20:10:34.000000000 +0200
+++ mercurial-5.9.1/tests/test-archive.t	2025-03-20 19:57:58.386149925 +0100
@@ -135,22 +135,22 @@
   body: size=506, sha1=70926a04cb8887d0bcccf5380488100a10222def (py38 no-py39 !)
   body: size=505, sha1=eb823c293bedff0df4070b854e2c5cbb06d6ec62 (py39 !)
   % tar.bz2 and zip disallowed should both give 403
-  403 Archive type not allowed: bz2
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
-  403 Archive type not allowed: zip
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
   $ test_archtype bz2 tar.bz2 zip tar.gz
   % bz2 allowed should give 200
   200 Script output follows
@@ -165,22 +165,22 @@
   body: size=506, sha1=1bd1f8e8d3701704bd4385038bd9c09b81c77f4e (py38 no-py39 !)
   body: size=503, sha1=2d8ce8bb3816603b9683a1804a5a02c11224cb01 (py39 !)
   % zip and tar.gz disallowed should both give 403
-  403 Archive type not allowed: zip
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
-  403 Archive type not allowed: gz
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
   $ test_archtype zip zip tar.gz tar.bz2
   % zip allowed should give 200
   200 Script output follows
@@ -193,22 +193,22 @@
   
   body: size=(1377|1461|1489), sha1=(677b14d3d048778d5eb5552c14a67e6192068650|be6d3983aa13dfe930361b2569291cdedd02b537|1897e496871aa89ad685a92b936f5fa0d008b9e8) (re)
   % tar.gz and tar.bz2 disallowed should both give 403
-  403 Archive type not allowed: gz
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
-  403 Archive type not allowed: bz2
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
 
 check http return codes (with deprecated option)
 
@@ -226,22 +226,22 @@
   body: size=506, sha1=70926a04cb8887d0bcccf5380488100a10222def (py38 no-py39 !)
   body: size=505, sha1=eb823c293bedff0df4070b854e2c5cbb06d6ec62 (py39 !)
   % tar.bz2 and zip disallowed should both give 403
-  403 Archive type not allowed: bz2
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
-  403 Archive type not allowed: zip
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
   $ test_archtype_deprecated bz2 tar.bz2 zip tar.gz
   % bz2 allowed should give 200
   200 Script output follows
@@ -256,22 +256,22 @@
   body: size=506, sha1=1bd1f8e8d3701704bd4385038bd9c09b81c77f4e (py38 no-py39 !)
   body: size=503, sha1=2d8ce8bb3816603b9683a1804a5a02c11224cb01 (py39 !)
   % zip and tar.gz disallowed should both give 403
-  403 Archive type not allowed: zip
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
-  403 Archive type not allowed: gz
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
   $ test_archtype_deprecated zip zip tar.gz tar.bz2
   % zip allowed should give 200
   200 Script output follows
@@ -284,22 +284,22 @@
   
   body: size=(1377|1461|1489), sha1=(677b14d3d048778d5eb5552c14a67e6192068650|be6d3983aa13dfe930361b2569291cdedd02b537|1897e496871aa89ad685a92b936f5fa0d008b9e8) (re)
   % tar.gz and tar.bz2 disallowed should both give 403
-  403 Archive type not allowed: gz
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
-  403 Archive type not allowed: bz2
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
+  403 Archive type not allowed
   content-type: text/html; charset=ascii
   date: $HTTP_DATE$
   etag: W/"*" (glob)
   server: testing stub value
   transfer-encoding: chunked
   
-  body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
+  body: size=1446, sha1=023cb60af79cf672217fbae8ecf20ad4b7472c9d
 
   $ echo "allow-archive = gz bz2 zip" >> .hg/hgrc
   $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
@@ -315,7 +315,7 @@
 invalid arch type should give 404
 
   $ get-with-headers.py localhost:$HGPORT "archive/tip.invalid" | head -n 1
-  404 Unsupported archive type: None
+  404 Unsupported archive type
 
   $ TIP=`hg id -v | cut -f1 -d' '`
   $ QTIP=`hg id -q`
@@ -387,12 +387,12 @@
 test that we detect file patterns that match no files
 
   $ "$PYTHON" getarchive.py "$TIP" gz foobar
-  HTTP Error 404: file(s) not found: foobar
+  HTTP Error 404: file(s) not found
 
 test that we reject unsafe patterns
 
   $ "$PYTHON" getarchive.py "$TIP" gz relre:baz
-  HTTP Error 404: file(s) not found: relre:baz
+  HTTP Error 404: file(s) not found
 
   $ killdaemons.py
 
Index: mercurial-5.9.1/tests/test-hgweb.t
===================================================================
--- mercurial-5.9.1.orig/tests/test-hgweb.t	2021-08-30 20:10:34.000000000 +0200
+++ mercurial-5.9.1/tests/test-hgweb.t	2025-03-20 19:57:58.386430353 +0100
@@ -122,25 +122,25 @@
   400* (glob)
   
   
-  error: no such method: spam
+  error: method not found
   [1]
 
   $ get-with-headers.py --headeronly localhost:$HGPORT '?cmd=spam'
-  400 no such method: spam
+  400 method not found
   [1]
 
 should give a 400 - bad command as a part of url path (issue4071)
 
   $ get-with-headers.py --headeronly localhost:$HGPORT 'spam'
-  400 no such method: spam
+  400 method not found
   [1]
 
   $ get-with-headers.py --headeronly localhost:$HGPORT 'raw-spam'
-  400 no such method: spam
+  400 method not found
   [1]
 
   $ get-with-headers.py --headeronly localhost:$HGPORT 'spam/tip/foo'
-  400 no such method: spam
+  400 method not found
   [1]
 
 should give a 404 - file does not exist
Index: mercurial-5.9.1/tests/test-lfs-serve-access.t
===================================================================
--- mercurial-5.9.1.orig/tests/test-lfs-serve-access.t	2021-08-30 20:10:35.000000000 +0200
+++ mercurial-5.9.1/tests/test-lfs-serve-access.t	2025-03-20 19:57:58.386590855 +0100
@@ -30,7 +30,7 @@
   $ hg -R client push http://localhost:$HGPORT
   pushing to http://localhost:$HGPORT/
   searching for changes
-  abort: LFS HTTP error: HTTP Error 400: no such method: .git
+  abort: LFS HTTP error: HTTP Error 400: method not found
   (check that lfs serving is enabled on http://localhost:$HGPORT/.git/info/lfs and "upload" is supported)
   [50]
 
@@ -52,7 +52,7 @@
   added 1 changesets with 1 changes to 1 files
   new changesets 525251863cad
   updating to branch default
-  abort: LFS HTTP error: HTTP Error 400: no such method: .git
+  abort: LFS HTTP error: HTTP Error 400: method not found
   (check that lfs serving is enabled on http://localhost:$HGPORT/.git/info/lfs and "download" is supported)
   [50]
 
Index: mercurial-5.9.1/tests/test-remotefilelog-http.t
===================================================================
--- mercurial-5.9.1.orig/tests/test-remotefilelog-http.t	2021-08-30 20:10:35.000000000 +0200
+++ mercurial-5.9.1/tests/test-remotefilelog-http.t	2025-03-20 19:57:58.386696094 +0100
@@ -44,9 +44,9 @@
   x_rfl_getflogheads
 
   $ get-with-headers.py localhost:$HGPORT '?cmd=this-command-does-not-exist' | head -n 1
-  400 no such method: this-command-does-not-exist
+  400 method not found
   $ get-with-headers.py localhost:$HGPORT '?cmd=x_rfl_getfiles' | head -n 1
-  400 no such method: x_rfl_getfiles
+  400 method not found
 
 Verify serving from a shallow clone doesn't allow for remotefile
 fetches. This also serves to test the error handling for our batchable
openSUSE Build Service is sponsored by