ref: cb8df31ee09f75a8ec7d6fad01a78249309b757f
parent: 13f131f5d572bec665e09cf49b504e195f0f7c7d
author: Ori Bernstein <ori@eigenstate.org>
date: Sat Feb 15 15:53:37 EST 2025
git/pull: validate and prevent rc from checking reference names (thanks falsifian) 1. If the remote $origin has refs under refs/remotes/$origin, these should be ignored, but instead, git/pull saves them to .git/refs/remotes/$origin, possibly overwriting what it found under refs/heads in the remote. As a concrete example, the main branch for https://github.com/Perl/perl5.git is called blead. If I run git/pull in a local checkout, I end up with an outdated blead because for some reason the github repo has its own refs/remotes/origin/blead, and git/get prints it after refs/heads/blead. 2. If the remote path has special characters like '}', the remote server could probably trick git/pull into executing arbitrary rc commands. I haven't actually verified this is possible, but git/pull is pasting the output of git/get unescaped into an rc command, and I couldn't see any safety checking for that value in git/get.c. This patch fixes both issues.
--- a/sys/src/cmd/git/get.c
+++ b/sys/src/cmd/git/get.c
@@ -7,7 +7,21 @@
char *upstream = "origin";
int listonly;
+/*
+ * Checks the rules for a refname at
+ * git/Documentation/protocol-common.txt
+ */
int
+okrefname(char *s)
+{
+ if(strcmp(s, "HEAD") == 0)
+ return 1;
+ if(strncmp(s, "refs/", 5) == 0)
+ return okref(s);
+ return 0;
+}
+
+int
resolveremote(Hash *h, char *ref)
{
char buf[128], *s;
@@ -247,6 +261,8 @@
getfields(buf, sp, nelem(sp), 1, " \t\n\r");
if(strstr(sp[1], "^{}"))
continue;
+ if(!okrefname(sp[1]))
+ sysfatal("remote side sent invalid ref: %s", sp[1]);
if(fetchbranch && !branchmatch(sp[1], fetchbranch))
continue;
if(refsz == nref + 1){
--- a/sys/src/cmd/git/git.h
+++ b/sys/src/cmd/git/git.h
@@ -338,6 +338,7 @@
int writephase(Conn *);
void closeconn(Conn *);
void parsecaps(char *, Conn *);
+int okref(char*);
/* queues */
void qinit(Objq*);
--- a/sys/src/cmd/git/pull
+++ b/sys/src/cmd/git/pull
@@ -10,14 +10,14 @@
if(! ~ $#debug 0)
dflag='-d'
{git/get $dflag -u $upstream $url >[2=3] || die $status} | awk '
- /^remote/{
- if($2=="HEAD")
- next
+ /^remote refs\/(heads|tags)\//{
ref=$2
hash=$3
gsub("^refs/heads", "refs/remotes/'$upstream'", ref)
outfile = ".git/"ref
- system("mkdir -p `{basename -d "outfile"}");
+ print outfile > "/env/outfile"
+ system("mkdir -p `$nl{basename -d $outfile}");\
+ close("/env/outfile")
print hash > outfile;
close(outfile);
}
--- a/sys/src/cmd/git/util.c
+++ b/sys/src/cmd/git/util.c
@@ -513,3 +513,56 @@
sysfatal("corrupt qid: %s (%x)", s, *e);
return q;
}
+
+/*
+ * Checks the rules for valid ref names, as defined in
+ * git/Documentation/protocol-common.txt.
+ * It does not check that the ref begins with refs/
+ * or is called HEAD, since that's only needed in the
+ * clone protocol.
+ */
+int
+okref(char *ref)
+{
+ int slashed;
+ char *p;
+
+ slashed = 0;
+ if(*ref == '/')
+ return 0;
+ for(p = ref; *p != 0; p++) {
+ switch(*p){
+ case '.':
+ if(p[1]== 0 || p[1] == '.')
+ return 0;
+ if(strcmp(p, ".lock") == 0)
+ return 0;
+ break;
+ case '/':
+ if(p[1] == 0 || p[1] == '.')
+ return 0;
+ slashed = 1;
+ break;
+ case '@':
+ if(p[1] == '{')
+ return 0;
+ break;
+ case ' ':
+ case '~':
+ case '^':
+ case ':':
+ case '?':
+ case '*':
+ case '[':
+ case '\\':
+ case 0x7f: /* DEL */
+ return 0;
+ default:
+ if(*p < 0x20)
+ return 0;
+ }
+ }
+ if(ref == p)
+ return 0;
+ return slashed;
+}
--
⑨