From: Debian Python Team Date: Thu, 20 Mar 2025 12:56:44 +0000 (+0100) Subject: CVE-2025-2361 X-Git-Tag: archive/raspbian/6.3.2-1+rpi1+deb12u1^2~2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=5197688cebef882c527cb79bc18bef7f3bfb55e9;p=mercurial.git CVE-2025-2361 # 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. Gbp-Pq: Name CVE-2025-2361.patch --- diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py index f688544..e640cd4 100644 --- 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 diff --git a/mercurial/hgweb/webcommands.py b/mercurial/hgweb/webcommands.py index 615ab2b..92927ca 100644 --- 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_] diff --git a/tests/test-archive.t b/tests/test-archive.t index bdad284..bab2eb5 100644 --- 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 option) 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 option) 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 option) 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 directories and files 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 diff --git a/tests/test-hgweb.t b/tests/test-hgweb.t index 459cab3..03213cd 100644 --- 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 diff --git a/tests/test-lfs-serve-access.t b/tests/test-lfs-serve-access.t index db038ea..66d4a72 100644 --- 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] diff --git a/tests/test-remotefilelog-http.t b/tests/test-remotefilelog-http.t index f5c4a67..c67b110 100644 --- a/tests/test-remotefilelog-http.t +++ b/tests/test-remotefilelog-http.t @@ -44,9 +44,9 @@ as the getfile method it offers doesn't work with http. 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