File hg-CVE-2025-2361.patch of Package mercurial.38124
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 | 26 +++++++++++++-------------
tests/test-hgweb.t | 10 +++++-----
4 files changed, 36 insertions(+), 26 deletions(-)
Index: mercurial-4.5.2/mercurial/hgweb/hgweb_mod.py
===================================================================
--- mercurial-4.5.2.orig/mercurial/hgweb/hgweb_mod.py 2018-03-06 20:19:51.000000000 +0100
+++ mercurial-4.5.2/mercurial/hgweb/hgweb_mod.py 2025-03-20 19:43:52.534289516 +0100
@@ -437,7 +437,10 @@
if rctx.configbool('web', 'cache') and not rctx.nonce:
caching(self, req) # sets ETag header or raises NOT_MODIFIED
if cmd not in webcommands.__all__:
- msg = 'no such method: %s' % cmd
+ msg = '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)
elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
rctx.ctype = ctype
Index: mercurial-4.5.2/mercurial/hgweb/webcommands.py
===================================================================
--- mercurial-4.5.2.orig/mercurial/hgweb/webcommands.py 2018-03-06 20:19:51.000000000 +0100
+++ mercurial-4.5.2/mercurial/hgweb/webcommands.py 2025-03-20 19:46:05.728244255 +0100
@@ -522,7 +522,9 @@
h[None] = None # denotes files present
if mf and not files and not dirs:
- raise ErrorResponse(HTTP_NOT_FOUND, '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(**map):
for f in sorted(files):
@@ -1108,12 +1110,15 @@
key = req.form['node'][0]
if type_ not in web.archivespecs:
- msg = 'Unsupported archive type: %s' % 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("web", "allow" + type_))):
- msg = '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(r"\W+", "-", os.path.basename(web.reponame))
@@ -1133,8 +1138,10 @@
if pats:
files = [f for f in ctx.manifest().keys() if match(f)]
if not files:
- raise ErrorResponse(HTTP_NOT_FOUND,
- 'file(s) not found: %s' % file[0])
+ # /!\ 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 = web.archivespecs[type_]
headers = [
Index: mercurial-4.5.2/tests/test-archive.t
===================================================================
--- mercurial-4.5.2.orig/tests/test-archive.t 2018-03-06 20:19:51.000000000 +0100
+++ mercurial-4.5.2/tests/test-archive.t 2025-03-20 19:42:22.316324005 +0100
@@ -122,20 +122,20 @@
% gz allowed should give 200
200 Script output follows
% tar.bz2 and zip disallowed should both give 403
- 403 Archive type not allowed: bz2
- 403 Archive type not allowed: zip
+ 403 Archive type not allowed
+ 403 Archive type not allowed
$ test_archtype bz2 tar.bz2 zip tar.gz
% bz2 allowed should give 200
200 Script output follows
% zip and tar.gz disallowed should both give 403
- 403 Archive type not allowed: zip
- 403 Archive type not allowed: gz
+ 403 Archive type not allowed
+ 403 Archive type not allowed
$ test_archtype zip zip tar.gz tar.bz2
% zip allowed should give 200
200 Script output follows
% tar.gz and tar.bz2 disallowed should both give 403
- 403 Archive type not allowed: gz
- 403 Archive type not allowed: bz2
+ 403 Archive type not allowed
+ 403 Archive type not allowed
check http return codes (with deprecated option)
@@ -143,20 +143,20 @@
% gz allowed should give 200
200 Script output follows
% tar.bz2 and zip disallowed should both give 403
- 403 Archive type not allowed: bz2
- 403 Archive type not allowed: zip
+ 403 Archive type not allowed
+ 403 Archive type not allowed
$ test_archtype_deprecated bz2 tar.bz2 zip tar.gz
% bz2 allowed should give 200
200 Script output follows
% zip and tar.gz disallowed should both give 403
- 403 Archive type not allowed: zip
- 403 Archive type not allowed: gz
+ 403 Archive type not allowed
+ 403 Archive type not allowed
$ test_archtype_deprecated zip zip tar.gz tar.bz2
% zip allowed should give 200
200 Script output follows
% tar.gz and tar.bz2 disallowed should both give 403
- 403 Archive type not allowed: gz
- 403 Archive type not allowed: bz2
+ 403 Archive type not allowed
+ 403 Archive type not allowed
$ echo "allow_archive = gz bz2 zip" >> .hg/hgrc
$ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
@@ -172,7 +172,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`
Index: mercurial-4.5.2/tests/test-hgweb.t
===================================================================
--- mercurial-4.5.2.orig/tests/test-hgweb.t 2018-03-06 20:19:51.000000000 +0100
+++ mercurial-4.5.2/tests/test-hgweb.t 2025-03-20 19:39:27.623009421 +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