Version in base suite: 6.3.2-1 Base version: mercurial_6.3.2-1 Target version: mercurial_6.3.2-1+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/m/mercurial/mercurial_6.3.2-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/m/mercurial/mercurial_6.3.2-1+deb12u1.dsc changelog | 8 patches/CVE-2025-2361.patch | 341 ++++++++++++++++++++++++++++++ patches/patchbomb-ambiguous-address.patch | 36 +++ patches/series | 2 4 files changed, 387 insertions(+) diff -Nru mercurial-6.3.2/debian/changelog mercurial-6.3.2/debian/changelog --- mercurial-6.3.2/debian/changelog 2023-02-20 17:44:46.000000000 +0000 +++ mercurial-6.3.2/debian/changelog 2025-03-20 12:56:44.000000000 +0000 @@ -1,3 +1,11 @@ +mercurial (6.3.2-1+deb12u1) bookworm-security; urgency=high + + * CVE-2025-2361: reflected XSS in hgweb (closes: #1100899) + * patchbomb: don't test ambiguous address (fixes FTBFS after python's + fix for CVE-2023-27043). + + -- Julien Cristau Thu, 20 Mar 2025 13:56:44 +0100 + mercurial (6.3.2-1) sid; urgency=medium * New upstream bugfix release. diff -Nru mercurial-6.3.2/debian/patches/CVE-2025-2361.patch mercurial-6.3.2/debian/patches/CVE-2025-2361.patch --- mercurial-6.3.2/debian/patches/CVE-2025-2361.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-6.3.2/debian/patches/CVE-2025-2361.patch 2025-03-20 09:50:44.000000000 +0000 @@ -0,0 +1,341 @@ +# HG changeset patch +# User Raphaël Gomès +# 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(-) + +--- a/mercurial/hgweb/hgweb_mod.py ++++ b/mercurial/hgweb/hgweb_mod.py +@@ -455,7 +455,10 @@ class hgweb: + 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 +--- a/mercurial/hgweb/webcommands.py ++++ b/mercurial/hgweb/webcommands.py +@@ -585,7 +585,9 @@ def manifest(web): + 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): +@@ -1255,11 +1257,15 @@ def archive(web): + 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)) +@@ -1278,9 +1284,10 @@ def archive(web): + 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_] + +--- a/tests/test-archive.t ++++ b/tests/test-archive.t +@@ -135,22 +135,22 @@ check http return codes + 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 @@ check http return codes + 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 @@ check http return codes + + 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 @@ check http return codes (with deprecated + 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 @@ check http return codes (with deprecated + 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 @@ check http return codes (with deprecated + + 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 @@ check archive links' order + 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` +@@ -386,12 +386,12 @@ test that we can download single directo + 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 + +--- a/tests/test-hgweb.t ++++ b/tests/test-hgweb.t +@@ -122,25 +122,25 @@ should give a 400 - bad command + 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 +--- a/tests/test-lfs-serve-access.t ++++ b/tests/test-lfs-serve-access.t +@@ -30,7 +30,7 @@ Uploads fail... + $ 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 @@ Downloads fail... + 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] + +--- a/tests/test-remotefilelog-http.t ++++ b/tests/test-remotefilelog-http.t +@@ -44,9 +44,9 @@ as the getfile method it offers doesn't + 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 diff -Nru mercurial-6.3.2/debian/patches/patchbomb-ambiguous-address.patch mercurial-6.3.2/debian/patches/patchbomb-ambiguous-address.patch --- mercurial-6.3.2/debian/patches/patchbomb-ambiguous-address.patch 1970-01-01 00:00:00.000000000 +0000 +++ mercurial-6.3.2/debian/patches/patchbomb-ambiguous-address.patch 2025-03-20 12:56:44.000000000 +0000 @@ -0,0 +1,36 @@ +# HG changeset patch +# User Raphaël Gomès +# Date 1729684194 -7200 +# Wed Oct 23 13:49:54 2024 +0200 +# Branch stable +# Node ID d906406658a947ab64b34302df15be21e928ba24 +# Parent e760a36a601336f75016e5c2bbc5a53da2ea7182 +patchbomb: don't test ambiguous address + +This is a bug in Python's `email` package and shouldn't be relied on. +Python 3.12 has fixed this problem¹ and raises an exception. We keep the +multiple `-t` because this is still relevant for testing. + +[1] https://github.com/python/cpython/issues/102988 +[2] https://docs.python.org/3/whatsnew/changelog.html + +--- a/tests/test-patchbomb.t ++++ b/tests/test-patchbomb.t +@@ -2377,7 +2377,7 @@ test multiple flags for multiple patches + + + test multi-address parsing: +- $ hg email --date '1980-1-1 0:1' -m tmp.mbox -f quux -t 'spam' \ ++ $ hg email --date '1980-1-1 0:1' -m tmp.mbox -f quux -t 'spam' \ + > -t toast -c 'foo,bar@example.com' -c '"A, B <>" ' -s test -r 0 \ + > --config email.bcc='"Quux, A." ' + this patch series consists of 1 patches. +@@ -2398,7 +2398,7 @@ test multi-address parsing: + User-Agent: Mercurial-patchbomb/* (glob) + Date: Tue, 01 Jan 1980 00:01:00 +0000 + From: quux +- To: =?iso-8859-1?q?spam?= , eggs, toast (py3 !) ++ To: =?iso-8859-1?q?spam?= , toast (py3 !) + Cc: foo, bar@example.com, =?iso-8859-1?q?A=2C_B_=3C=3E?= (py3 !) + Bcc: =?iso-8859-1?q?Quux=2C_A=2E?= (py3 !) + diff -Nru mercurial-6.3.2/debian/patches/series mercurial-6.3.2/debian/patches/series --- mercurial-6.3.2/debian/patches/series 2023-02-20 17:44:25.000000000 +0000 +++ mercurial-6.3.2/debian/patches/series 2025-03-20 12:56:44.000000000 +0000 @@ -8,3 +8,5 @@ openssl_3_cipher_tlsv1.patch test-hghave-testrepo.patch cgitb.patch +CVE-2025-2361.patch +patchbomb-ambiguous-address.patch