Fossil SCM
(cherry-pick): Fix the SSH sync protocol to avoid "ssh" command-line option injection attacks such as those fixed in Git 2.14.1, Mercurial 4.2.3, and Subversion 1.9.7. As "ssh://" URLs cannot be buried out of sight in Fossil, the vulnerability does not appear to be as severe as in those other systems. (cherry-pick): Enhance the ssh:// URL to be cautious about the fossil= query parameter. Only commands "fossil" and "echo" (with an optional path) are accepted.
Commit
1f18d23d760c37c78310188ae1db3b9d6e7a4b93d6537df99c2ba3d57075ee45
Parent
dad3706248b945e…
2 files changed
+27
-15
+27
-15
+27
-15
| --- src/http_transport.c | ||
| +++ src/http_transport.c | ||
| @@ -73,10 +73,25 @@ | ||
| 73 | 73 | if( resetFlag ){ |
| 74 | 74 | transport.nSent = 0; |
| 75 | 75 | transport.nRcvd = 0; |
| 76 | 76 | } |
| 77 | 77 | } |
| 78 | + | |
| 79 | +/* | |
| 80 | +** Check zFossil to see if it is a reasonable "fossil" command to | |
| 81 | +** run on the server. Do not allow an attacker to substitute something | |
| 82 | +** like "/bin/rm". | |
| 83 | +*/ | |
| 84 | +static int is_safe_fossil_command(const char *zFossil){ | |
| 85 | + static const char *azSafe[] = { "*/fossil", "*/echo" }; | |
| 86 | + int i; | |
| 87 | + for(i=0; i<sizeof(azSafe)/sizeof(azSafe[0]); i++){ | |
| 88 | + if( sqlite3_strglob(azSafe[i], zFossil)==0 ) return 1; | |
| 89 | + if( strcmp(azSafe[i]+2, zFossil)==0 ) return 1; | |
| 90 | + } | |
| 91 | + return 0; | |
| 92 | +} | |
| 78 | 93 | |
| 79 | 94 | /* |
| 80 | 95 | ** Default SSH command |
| 81 | 96 | */ |
| 82 | 97 | #ifdef _WIN32 |
| @@ -93,11 +108,10 @@ | ||
| 93 | 108 | ** to talk to the remote machine. |
| 94 | 109 | */ |
| 95 | 110 | char *zSsh; /* The base SSH command */ |
| 96 | 111 | Blob zCmd; /* The SSH command */ |
| 97 | 112 | char *zHost; /* The host name to contact */ |
| 98 | - int n; /* Size of prefix string */ | |
| 99 | 113 | |
| 100 | 114 | socket_ssh_resolve_addr(pUrlData); |
| 101 | 115 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 102 | 116 | blob_init(&zCmd, zSsh, -1); |
| 103 | 117 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| @@ -105,33 +119,31 @@ | ||
| 105 | 119 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 106 | 120 | #else |
| 107 | 121 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 108 | 122 | #endif |
| 109 | 123 | } |
| 110 | - if( g.fSshTrace ){ | |
| 111 | - fossil_force_newline(); | |
| 112 | - fossil_print("%s", blob_str(&zCmd)); /* Show the base of the SSH command */ | |
| 113 | - } | |
| 114 | 124 | if( pUrlData->user && pUrlData->user[0] ){ |
| 115 | 125 | zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); |
| 126 | + blob_append_escaped_arg(&zCmd, zHost); | |
| 127 | + fossil_free(zHost); | |
| 116 | 128 | }else{ |
| 117 | - zHost = mprintf("%s", pUrlData->name); | |
| 129 | + blob_append_escaped_arg(&zCmd, pUrlData->name); | |
| 130 | + } | |
| 131 | + if( !is_safe_fossil_command(pUrlData->fossil) ){ | |
| 132 | + fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " | |
| 133 | + "the server.", pUrlData->fossil); | |
| 118 | 134 | } |
| 119 | - n = blob_size(&zCmd); | |
| 120 | - blob_append(&zCmd, " ", 1); | |
| 121 | - shell_escape(&zCmd, zHost); | |
| 122 | - blob_append(&zCmd, " ", 1); | |
| 123 | - shell_escape(&zCmd, mprintf("%s", pUrlData->fossil)); | |
| 135 | + blob_append_escaped_arg(&zCmd, pUrlData->fossil); | |
| 124 | 136 | blob_append(&zCmd, " test-http", 10); |
| 125 | 137 | if( pUrlData->path && pUrlData->path[0] ){ |
| 126 | - blob_append(&zCmd, " ", 1); | |
| 127 | - shell_escape(&zCmd, mprintf("%s", pUrlData->path)); | |
| 138 | + blob_append_escaped_arg(&zCmd, pUrlData->path); | |
| 139 | + }else{ | |
| 140 | + fossil_fatal("ssh:// URI does not specify a path to the repository"); | |
| 128 | 141 | } |
| 129 | 142 | if( g.fSshTrace ){ |
| 130 | - fossil_print("%s\n", blob_str(&zCmd)+n); /* Show tail of SSH command */ | |
| 143 | + fossil_print("%s\n", blob_str(&zCmd)); /* Show the whole SSH command */ | |
| 131 | 144 | } |
| 132 | - free(zHost); | |
| 133 | 145 | popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid); |
| 134 | 146 | if( sshPid==0 ){ |
| 135 | 147 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 136 | 148 | } |
| 137 | 149 | blob_reset(&zCmd); |
| 138 | 150 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -73,10 +73,25 @@ | |
| 73 | if( resetFlag ){ |
| 74 | transport.nSent = 0; |
| 75 | transport.nRcvd = 0; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /* |
| 80 | ** Default SSH command |
| 81 | */ |
| 82 | #ifdef _WIN32 |
| @@ -93,11 +108,10 @@ | |
| 93 | ** to talk to the remote machine. |
| 94 | */ |
| 95 | char *zSsh; /* The base SSH command */ |
| 96 | Blob zCmd; /* The SSH command */ |
| 97 | char *zHost; /* The host name to contact */ |
| 98 | int n; /* Size of prefix string */ |
| 99 | |
| 100 | socket_ssh_resolve_addr(pUrlData); |
| 101 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 102 | blob_init(&zCmd, zSsh, -1); |
| 103 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| @@ -105,33 +119,31 @@ | |
| 105 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 106 | #else |
| 107 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 108 | #endif |
| 109 | } |
| 110 | if( g.fSshTrace ){ |
| 111 | fossil_force_newline(); |
| 112 | fossil_print("%s", blob_str(&zCmd)); /* Show the base of the SSH command */ |
| 113 | } |
| 114 | if( pUrlData->user && pUrlData->user[0] ){ |
| 115 | zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); |
| 116 | }else{ |
| 117 | zHost = mprintf("%s", pUrlData->name); |
| 118 | } |
| 119 | n = blob_size(&zCmd); |
| 120 | blob_append(&zCmd, " ", 1); |
| 121 | shell_escape(&zCmd, zHost); |
| 122 | blob_append(&zCmd, " ", 1); |
| 123 | shell_escape(&zCmd, mprintf("%s", pUrlData->fossil)); |
| 124 | blob_append(&zCmd, " test-http", 10); |
| 125 | if( pUrlData->path && pUrlData->path[0] ){ |
| 126 | blob_append(&zCmd, " ", 1); |
| 127 | shell_escape(&zCmd, mprintf("%s", pUrlData->path)); |
| 128 | } |
| 129 | if( g.fSshTrace ){ |
| 130 | fossil_print("%s\n", blob_str(&zCmd)+n); /* Show tail of SSH command */ |
| 131 | } |
| 132 | free(zHost); |
| 133 | popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid); |
| 134 | if( sshPid==0 ){ |
| 135 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 136 | } |
| 137 | blob_reset(&zCmd); |
| 138 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -73,10 +73,25 @@ | |
| 73 | if( resetFlag ){ |
| 74 | transport.nSent = 0; |
| 75 | transport.nRcvd = 0; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /* |
| 80 | ** Check zFossil to see if it is a reasonable "fossil" command to |
| 81 | ** run on the server. Do not allow an attacker to substitute something |
| 82 | ** like "/bin/rm". |
| 83 | */ |
| 84 | static int is_safe_fossil_command(const char *zFossil){ |
| 85 | static const char *azSafe[] = { "*/fossil", "*/echo" }; |
| 86 | int i; |
| 87 | for(i=0; i<sizeof(azSafe)/sizeof(azSafe[0]); i++){ |
| 88 | if( sqlite3_strglob(azSafe[i], zFossil)==0 ) return 1; |
| 89 | if( strcmp(azSafe[i]+2, zFossil)==0 ) return 1; |
| 90 | } |
| 91 | return 0; |
| 92 | } |
| 93 | |
| 94 | /* |
| 95 | ** Default SSH command |
| 96 | */ |
| 97 | #ifdef _WIN32 |
| @@ -93,11 +108,10 @@ | |
| 108 | ** to talk to the remote machine. |
| 109 | */ |
| 110 | char *zSsh; /* The base SSH command */ |
| 111 | Blob zCmd; /* The SSH command */ |
| 112 | char *zHost; /* The host name to contact */ |
| 113 | |
| 114 | socket_ssh_resolve_addr(pUrlData); |
| 115 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 116 | blob_init(&zCmd, zSsh, -1); |
| 117 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| @@ -105,33 +119,31 @@ | |
| 119 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 120 | #else |
| 121 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 122 | #endif |
| 123 | } |
| 124 | if( pUrlData->user && pUrlData->user[0] ){ |
| 125 | zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); |
| 126 | blob_append_escaped_arg(&zCmd, zHost); |
| 127 | fossil_free(zHost); |
| 128 | }else{ |
| 129 | blob_append_escaped_arg(&zCmd, pUrlData->name); |
| 130 | } |
| 131 | if( !is_safe_fossil_command(pUrlData->fossil) ){ |
| 132 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 133 | "the server.", pUrlData->fossil); |
| 134 | } |
| 135 | blob_append_escaped_arg(&zCmd, pUrlData->fossil); |
| 136 | blob_append(&zCmd, " test-http", 10); |
| 137 | if( pUrlData->path && pUrlData->path[0] ){ |
| 138 | blob_append_escaped_arg(&zCmd, pUrlData->path); |
| 139 | }else{ |
| 140 | fossil_fatal("ssh:// URI does not specify a path to the repository"); |
| 141 | } |
| 142 | if( g.fSshTrace ){ |
| 143 | fossil_print("%s\n", blob_str(&zCmd)); /* Show the whole SSH command */ |
| 144 | } |
| 145 | popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid); |
| 146 | if( sshPid==0 ){ |
| 147 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 148 | } |
| 149 | blob_reset(&zCmd); |
| 150 |
+27
-15
| --- src/http_transport.c | ||
| +++ src/http_transport.c | ||
| @@ -73,10 +73,25 @@ | ||
| 73 | 73 | if( resetFlag ){ |
| 74 | 74 | transport.nSent = 0; |
| 75 | 75 | transport.nRcvd = 0; |
| 76 | 76 | } |
| 77 | 77 | } |
| 78 | + | |
| 79 | +/* | |
| 80 | +** Check zFossil to see if it is a reasonable "fossil" command to | |
| 81 | +** run on the server. Do not allow an attacker to substitute something | |
| 82 | +** like "/bin/rm". | |
| 83 | +*/ | |
| 84 | +static int is_safe_fossil_command(const char *zFossil){ | |
| 85 | + static const char *azSafe[] = { "*/fossil", "*/echo" }; | |
| 86 | + int i; | |
| 87 | + for(i=0; i<sizeof(azSafe)/sizeof(azSafe[0]); i++){ | |
| 88 | + if( sqlite3_strglob(azSafe[i], zFossil)==0 ) return 1; | |
| 89 | + if( strcmp(azSafe[i]+2, zFossil)==0 ) return 1; | |
| 90 | + } | |
| 91 | + return 0; | |
| 92 | +} | |
| 78 | 93 | |
| 79 | 94 | /* |
| 80 | 95 | ** Default SSH command |
| 81 | 96 | */ |
| 82 | 97 | #ifdef _WIN32 |
| @@ -93,11 +108,10 @@ | ||
| 93 | 108 | ** to talk to the remote machine. |
| 94 | 109 | */ |
| 95 | 110 | char *zSsh; /* The base SSH command */ |
| 96 | 111 | Blob zCmd; /* The SSH command */ |
| 97 | 112 | char *zHost; /* The host name to contact */ |
| 98 | - int n; /* Size of prefix string */ | |
| 99 | 113 | |
| 100 | 114 | socket_ssh_resolve_addr(pUrlData); |
| 101 | 115 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 102 | 116 | blob_init(&zCmd, zSsh, -1); |
| 103 | 117 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| @@ -105,33 +119,31 @@ | ||
| 105 | 119 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 106 | 120 | #else |
| 107 | 121 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 108 | 122 | #endif |
| 109 | 123 | } |
| 110 | - if( g.fSshTrace ){ | |
| 111 | - fossil_force_newline(); | |
| 112 | - fossil_print("%s", blob_str(&zCmd)); /* Show the base of the SSH command */ | |
| 113 | - } | |
| 114 | 124 | if( pUrlData->user && pUrlData->user[0] ){ |
| 115 | 125 | zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); |
| 126 | + blob_append_escaped_arg(&zCmd, zHost); | |
| 127 | + fossil_free(zHost); | |
| 116 | 128 | }else{ |
| 117 | - zHost = mprintf("%s", pUrlData->name); | |
| 129 | + blob_append_escaped_arg(&zCmd, pUrlData->name); | |
| 130 | + } | |
| 131 | + if( !is_safe_fossil_command(pUrlData->fossil) ){ | |
| 132 | + fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " | |
| 133 | + "the server.", pUrlData->fossil); | |
| 118 | 134 | } |
| 119 | - n = blob_size(&zCmd); | |
| 120 | - blob_append(&zCmd, " ", 1); | |
| 121 | - shell_escape(&zCmd, zHost); | |
| 122 | - blob_append(&zCmd, " ", 1); | |
| 123 | - shell_escape(&zCmd, mprintf("%s", pUrlData->fossil)); | |
| 135 | + blob_append_escaped_arg(&zCmd, pUrlData->fossil); | |
| 124 | 136 | blob_append(&zCmd, " test-http", 10); |
| 125 | 137 | if( pUrlData->path && pUrlData->path[0] ){ |
| 126 | - blob_append(&zCmd, " ", 1); | |
| 127 | - shell_escape(&zCmd, mprintf("%s", pUrlData->path)); | |
| 138 | + blob_append_escaped_arg(&zCmd, pUrlData->path); | |
| 139 | + }else{ | |
| 140 | + fossil_fatal("ssh:// URI does not specify a path to the repository"); | |
| 128 | 141 | } |
| 129 | 142 | if( g.fSshTrace ){ |
| 130 | - fossil_print("%s\n", blob_str(&zCmd)+n); /* Show tail of SSH command */ | |
| 143 | + fossil_print("%s\n", blob_str(&zCmd)); /* Show the whole SSH command */ | |
| 131 | 144 | } |
| 132 | - free(zHost); | |
| 133 | 145 | popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid); |
| 134 | 146 | if( sshPid==0 ){ |
| 135 | 147 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 136 | 148 | } |
| 137 | 149 | blob_reset(&zCmd); |
| 138 | 150 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -73,10 +73,25 @@ | |
| 73 | if( resetFlag ){ |
| 74 | transport.nSent = 0; |
| 75 | transport.nRcvd = 0; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /* |
| 80 | ** Default SSH command |
| 81 | */ |
| 82 | #ifdef _WIN32 |
| @@ -93,11 +108,10 @@ | |
| 93 | ** to talk to the remote machine. |
| 94 | */ |
| 95 | char *zSsh; /* The base SSH command */ |
| 96 | Blob zCmd; /* The SSH command */ |
| 97 | char *zHost; /* The host name to contact */ |
| 98 | int n; /* Size of prefix string */ |
| 99 | |
| 100 | socket_ssh_resolve_addr(pUrlData); |
| 101 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 102 | blob_init(&zCmd, zSsh, -1); |
| 103 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| @@ -105,33 +119,31 @@ | |
| 105 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 106 | #else |
| 107 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 108 | #endif |
| 109 | } |
| 110 | if( g.fSshTrace ){ |
| 111 | fossil_force_newline(); |
| 112 | fossil_print("%s", blob_str(&zCmd)); /* Show the base of the SSH command */ |
| 113 | } |
| 114 | if( pUrlData->user && pUrlData->user[0] ){ |
| 115 | zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); |
| 116 | }else{ |
| 117 | zHost = mprintf("%s", pUrlData->name); |
| 118 | } |
| 119 | n = blob_size(&zCmd); |
| 120 | blob_append(&zCmd, " ", 1); |
| 121 | shell_escape(&zCmd, zHost); |
| 122 | blob_append(&zCmd, " ", 1); |
| 123 | shell_escape(&zCmd, mprintf("%s", pUrlData->fossil)); |
| 124 | blob_append(&zCmd, " test-http", 10); |
| 125 | if( pUrlData->path && pUrlData->path[0] ){ |
| 126 | blob_append(&zCmd, " ", 1); |
| 127 | shell_escape(&zCmd, mprintf("%s", pUrlData->path)); |
| 128 | } |
| 129 | if( g.fSshTrace ){ |
| 130 | fossil_print("%s\n", blob_str(&zCmd)+n); /* Show tail of SSH command */ |
| 131 | } |
| 132 | free(zHost); |
| 133 | popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid); |
| 134 | if( sshPid==0 ){ |
| 135 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 136 | } |
| 137 | blob_reset(&zCmd); |
| 138 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -73,10 +73,25 @@ | |
| 73 | if( resetFlag ){ |
| 74 | transport.nSent = 0; |
| 75 | transport.nRcvd = 0; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /* |
| 80 | ** Check zFossil to see if it is a reasonable "fossil" command to |
| 81 | ** run on the server. Do not allow an attacker to substitute something |
| 82 | ** like "/bin/rm". |
| 83 | */ |
| 84 | static int is_safe_fossil_command(const char *zFossil){ |
| 85 | static const char *azSafe[] = { "*/fossil", "*/echo" }; |
| 86 | int i; |
| 87 | for(i=0; i<sizeof(azSafe)/sizeof(azSafe[0]); i++){ |
| 88 | if( sqlite3_strglob(azSafe[i], zFossil)==0 ) return 1; |
| 89 | if( strcmp(azSafe[i]+2, zFossil)==0 ) return 1; |
| 90 | } |
| 91 | return 0; |
| 92 | } |
| 93 | |
| 94 | /* |
| 95 | ** Default SSH command |
| 96 | */ |
| 97 | #ifdef _WIN32 |
| @@ -93,11 +108,10 @@ | |
| 108 | ** to talk to the remote machine. |
| 109 | */ |
| 110 | char *zSsh; /* The base SSH command */ |
| 111 | Blob zCmd; /* The SSH command */ |
| 112 | char *zHost; /* The host name to contact */ |
| 113 | |
| 114 | socket_ssh_resolve_addr(pUrlData); |
| 115 | zSsh = db_get("ssh-command", zDefaultSshCmd); |
| 116 | blob_init(&zCmd, zSsh, -1); |
| 117 | if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ |
| @@ -105,33 +119,31 @@ | |
| 119 | blob_appendf(&zCmd, " -P %d", pUrlData->port); |
| 120 | #else |
| 121 | blob_appendf(&zCmd, " -p %d", pUrlData->port); |
| 122 | #endif |
| 123 | } |
| 124 | if( pUrlData->user && pUrlData->user[0] ){ |
| 125 | zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); |
| 126 | blob_append_escaped_arg(&zCmd, zHost); |
| 127 | fossil_free(zHost); |
| 128 | }else{ |
| 129 | blob_append_escaped_arg(&zCmd, pUrlData->name); |
| 130 | } |
| 131 | if( !is_safe_fossil_command(pUrlData->fossil) ){ |
| 132 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 133 | "the server.", pUrlData->fossil); |
| 134 | } |
| 135 | blob_append_escaped_arg(&zCmd, pUrlData->fossil); |
| 136 | blob_append(&zCmd, " test-http", 10); |
| 137 | if( pUrlData->path && pUrlData->path[0] ){ |
| 138 | blob_append_escaped_arg(&zCmd, pUrlData->path); |
| 139 | }else{ |
| 140 | fossil_fatal("ssh:// URI does not specify a path to the repository"); |
| 141 | } |
| 142 | if( g.fSshTrace ){ |
| 143 | fossil_print("%s\n", blob_str(&zCmd)); /* Show the whole SSH command */ |
| 144 | } |
| 145 | popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid); |
| 146 | if( sshPid==0 ){ |
| 147 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 148 | } |
| 149 | blob_reset(&zCmd); |
| 150 |