hook: report untrusted hooks as failure (issue5110) (BC)
authorPierre-Yves David <pierre-yves.david@ens-lyon.org>
Thu, 14 Apr 2016 02:41:15 -0700
changeset 28938 ea1fec3e9aba
parent 28937 3112c5e18835
child 28939 ce01b4810aef
hook: report untrusted hooks as failure (issue5110) (BC) Before this patch, there was no way for a repository owner to ensure that validation hooks would be run by people with write access. If someone had write access but did not trust the user owning the repository, the config and its hook would simply be ignored. After this patch, hooks from untrusted configs are taken into account but never actually run. Instead they are reported as failures right away. This will ensure validation performed by a hook is not ignored. As a side effect writer can be forced to trust a repository hgrc by adding a 'pretxnopen.trust=true' hook to the file. This was discussed during the 3.8 sprint with Matt Mackall, Augie Fackler and Kevin Bullock.
mercurial/hook.py
tests/test-hook.t
--- a/mercurial/hook.py	Thu Apr 14 17:03:49 2016 -0700
+++ b/mercurial/hook.py	Thu Apr 14 02:41:15 2016 -0700
@@ -161,15 +161,28 @@
         ui.warn(_('warning: %s hook %s\n') % (name, desc))
     return r
 
+# represent an untrusted hook command
+_fromuntrusted = object()
+
 def _allhooks(ui):
     """return a list of (hook-id, cmd) pairs sorted by priority"""
     hooks = _hookitems(ui)
+    # Be careful in this section, propagating the real commands from untrusted
+    # sources would create a security vulnerability, make sure anything altered
+    # in that section uses "_fromuntrusted" as its command.
+    untrustedhooks = _hookitems(ui, _untrusted=True)
+    for name, value in untrustedhooks.items():
+        trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
+        if value != trustedvalue:
+            (lp, lo, lk, lv) = trustedvalue
+            hooks[name] = (lp, lo, lk, _fromuntrusted)
+    # (end of the security sensitive section)
     return [(k, v) for p, o, k, v in sorted(hooks.values())]
 
-def _hookitems(ui):
+def _hookitems(ui, _untrusted=False):
     """return all hooks items ready to be sorted"""
     hooks = {}
-    for name, cmd in ui.configitems('hooks'):
+    for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
         if not name.startswith('priority'):
             priority = ui.configint('hooks', 'priority.%s' % name, 0)
             hooks[name] = (-priority, len(hooks), name, cmd)
@@ -214,7 +227,15 @@
                     # files seem to be bogus, give up on redirecting (WSGI, etc)
                     pass
 
-            if callable(cmd):
+            if cmd is _fromuntrusted:
+                if throw:
+                    raise error.HookAbort(
+                        _('untrusted hook %s not executed') % name,
+                        hint = _("see 'hg help config.trusted'"))
+                ui.warn(_('warning: untrusted hook %s not executed\n') % name)
+                r = 1
+                raised = False
+            elif callable(cmd):
                 r, raised = _pythonhook(ui, repo, name, hname, cmd, args, throw)
             elif cmd.startswith('python:'):
                 if cmd.count(':') >= 2:
--- a/tests/test-hook.t	Thu Apr 14 17:03:49 2016 -0700
+++ b/tests/test-hook.t	Thu Apr 14 02:41:15 2016 -0700
@@ -794,7 +794,6 @@
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     b
   
-  $ cd ..
 
 pretxnclose hook failure should abort the transaction
 
@@ -816,3 +815,62 @@
   $ hg recover
   no interrupted transaction available
   [1]
+  $ cd ..
+
+Hook from untrusted hgrc are reported as failure
+================================================
+
+  $ cat << EOF > $TESTTMP/untrusted.py
+  > from mercurial import scmutil, util
+  > def uisetup(ui):
+  >     class untrustedui(ui.__class__):
+  >         def _trusted(self, fp, f):
+  >             if util.normpath(fp.name).endswith('untrusted/.hg/hgrc'):
+  >                 return False
+  >             return super(untrustedui, self)._trusted(fp, f)
+  >     ui.__class__ = untrustedui
+  > EOF
+  $ cat << EOF >> $HGRCPATH
+  > [extensions]
+  > untrusted=$TESTTMP/untrusted.py
+  > EOF
+  $ hg init untrusted
+  $ cd untrusted
+
+Non-blocking hook
+-----------------
+
+  $ cat << EOF >> .hg/hgrc
+  > [hooks]
+  > txnclose.testing=echo txnclose hook called
+  > EOF
+  $ touch a && hg commit -Aqm a
+  warning: untrusted hook txnclose not executed
+  $ hg log
+  changeset:   0:3903775176ed
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     a
+  
+
+Non-blocking hook
+-----------------
+
+  $ cat << EOF >> .hg/hgrc
+  > [hooks]
+  > pretxnclose.testing=echo pre-txnclose hook called
+  > EOF
+  $ touch b && hg commit -Aqm a
+  transaction abort!
+  rollback completed
+  abort: untrusted hook pretxnclose not executed
+  (see 'hg help config.trusted')
+  [255]
+  $ hg log
+  changeset:   0:3903775176ed
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     a
+