Skip to content

Commit a7b1ad3

Browse files
committed
Merge branch 'jk/fast-import-unsafe'
The `--export-marks` option of `git fast-import` is exposed also via the in-stream command `feature export-marks=...` and it allows overwriting arbitrary paths. This topic branch prevents the in-stream version, to prevent arbitrary file accesses by `git fast-import` streams coming from untrusted sources (e.g. in remote helpers that are based on `git fast-import`). This fixes CVE-2019-1348. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents d0832b2 + a52ed76 commit a7b1ad3

File tree

4 files changed

+95
-18
lines changed

4 files changed

+95
-18
lines changed

Documentation/git-fast-import.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ OPTIONS
5050
memory used by fast-import during this run. Showing this output
5151
is currently the default, but can be disabled with --quiet.
5252

53+
--allow-unsafe-features::
54+
Many command-line options can be provided as part of the
55+
fast-import stream itself by using the `feature` or `option`
56+
commands. However, some of these options are unsafe (e.g.,
57+
allowing fast-import to access the filesystem outside of the
58+
repository). These options are disabled by default, but can be
59+
allowed by providing this option on the command line. This
60+
currently impacts only the `export-marks`, `import-marks`, and
61+
`import-marks-if-exists` feature commands.
62+
+
63+
Only enable this option if you trust the program generating the
64+
fast-import stream! This option is enabled automatically for
65+
remote-helpers that use the `import` capability, as they are
66+
already trusted to run their own code.
67+
5368
Options for Frontends
5469
~~~~~~~~~~~~~~~~~~~~~
5570

fast-import.c

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ static uintmax_t next_mark;
366366
static struct strbuf new_data = STRBUF_INIT;
367367
static int seen_data_command;
368368
static int require_explicit_termination;
369+
static int allow_unsafe_features;
369370

370371
/* Signal handling */
371372
static volatile sig_atomic_t checkpoint_requested;
@@ -1861,6 +1862,12 @@ static void dump_marks(void)
18611862
if (!export_marks_file || (import_marks_file && !import_marks_file_done))
18621863
return;
18631864

1865+
if (safe_create_leading_directories_const(export_marks_file)) {
1866+
failure |= error_errno("unable to create leading directories of %s",
1867+
export_marks_file);
1868+
return;
1869+
}
1870+
18641871
if (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) {
18651872
failure |= error_errno("Unable to write marks file %s",
18661873
export_marks_file);
@@ -3228,7 +3235,6 @@ static void option_import_marks(const char *marks,
32283235
}
32293236

32303237
import_marks_file = make_fast_import_path(marks);
3231-
safe_create_leading_directories_const(import_marks_file);
32323238
import_marks_file_from_stream = from_stream;
32333239
import_marks_file_ignore_missing = ignore_missing;
32343240
}
@@ -3269,7 +3275,6 @@ static void option_active_branches(const char *branches)
32693275
static void option_export_marks(const char *marks)
32703276
{
32713277
export_marks_file = make_fast_import_path(marks);
3272-
safe_create_leading_directories_const(export_marks_file);
32733278
}
32743279

32753280
static void option_cat_blob_fd(const char *fd)
@@ -3312,28 +3317,40 @@ static int parse_one_option(const char *option)
33123317
option_active_branches(option);
33133318
} else if (skip_prefix(option, "export-pack-edges=", &option)) {
33143319
option_export_pack_edges(option);
3315-
} else if (starts_with(option, "quiet")) {
3320+
} else if (!strcmp(option, "quiet")) {
33163321
show_stats = 0;
3317-
} else if (starts_with(option, "stats")) {
3322+
} else if (!strcmp(option, "stats")) {
33183323
show_stats = 1;
3324+
} else if (!strcmp(option, "allow-unsafe-features")) {
3325+
; /* already handled during early option parsing */
33193326
} else {
33203327
return 0;
33213328
}
33223329

33233330
return 1;
33243331
}
33253332

3333+
static void check_unsafe_feature(const char *feature, int from_stream)
3334+
{
3335+
if (from_stream && !allow_unsafe_features)
3336+
die(_("feature '%s' forbidden in input without --allow-unsafe-features"),
3337+
feature);
3338+
}
3339+
33263340
static int parse_one_feature(const char *feature, int from_stream)
33273341
{
33283342
const char *arg;
33293343

33303344
if (skip_prefix(feature, "date-format=", &arg)) {
33313345
option_date_format(arg);
33323346
} else if (skip_prefix(feature, "import-marks=", &arg)) {
3347+
check_unsafe_feature("import-marks", from_stream);
33333348
option_import_marks(arg, from_stream, 0);
33343349
} else if (skip_prefix(feature, "import-marks-if-exists=", &arg)) {
3350+
check_unsafe_feature("import-marks-if-exists", from_stream);
33353351
option_import_marks(arg, from_stream, 1);
33363352
} else if (skip_prefix(feature, "export-marks=", &arg)) {
3353+
check_unsafe_feature(feature, from_stream);
33373354
option_export_marks(arg);
33383355
} else if (!strcmp(feature, "get-mark")) {
33393356
; /* Don't die - this feature is supported */
@@ -3460,6 +3477,20 @@ int cmd_main(int argc, const char **argv)
34603477
avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*));
34613478
marks = pool_calloc(1, sizeof(struct mark_set));
34623479

3480+
/*
3481+
* We don't parse most options until after we've seen the set of
3482+
* "feature" lines at the start of the stream (which allows the command
3483+
* line to override stream data). But we must do an early parse of any
3484+
* command-line options that impact how we interpret the feature lines.
3485+
*/
3486+
for (i = 1; i < argc; i++) {
3487+
const char *arg = argv[i];
3488+
if (*arg != '-' || !strcmp(arg, "--"))
3489+
break;
3490+
if (!strcmp(arg, "--allow-unsafe-features"))
3491+
allow_unsafe_features = 1;
3492+
}
3493+
34633494
global_argc = argc;
34643495
global_argv = argv;
34653496

t/t9300-fast-import.sh

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2106,12 +2106,27 @@ test_expect_success 'R: abort on receiving feature after data command' '
21062106
test_must_fail git fast-import <input
21072107
'
21082108

2109+
test_expect_success 'R: import-marks features forbidden by default' '
2110+
>git.marks &&
2111+
echo "feature import-marks=git.marks" >input &&
2112+
test_must_fail git fast-import <input &&
2113+
echo "feature import-marks-if-exists=git.marks" >input &&
2114+
test_must_fail git fast-import <input
2115+
'
2116+
21092117
test_expect_success 'R: only one import-marks feature allowed per stream' '
2118+
>git.marks &&
2119+
>git2.marks &&
21102120
cat >input <<-EOF &&
21112121
feature import-marks=git.marks
21122122
feature import-marks=git2.marks
21132123
EOF
21142124
2125+
test_must_fail git fast-import --allow-unsafe-features <input
2126+
'
2127+
2128+
test_expect_success 'R: export-marks feature forbidden by default' '
2129+
echo "feature export-marks=git.marks" >input &&
21152130
test_must_fail git fast-import <input
21162131
'
21172132

@@ -2125,19 +2140,29 @@ test_expect_success 'R: export-marks feature results in a marks file being creat
21252140
21262141
EOF
21272142
2128-
cat input | git fast-import &&
2143+
git fast-import --allow-unsafe-features <input &&
21292144
grep :1 git.marks
21302145
'
21312146

21322147
test_expect_success 'R: export-marks options can be overridden by commandline options' '
2133-
cat input | git fast-import --export-marks=other.marks &&
2134-
grep :1 other.marks
2148+
cat >input <<-\EOF &&
2149+
feature export-marks=feature-sub/git.marks
2150+
blob
2151+
mark :1
2152+
data 3
2153+
hi
2154+
2155+
EOF
2156+
git fast-import --allow-unsafe-features \
2157+
--export-marks=cmdline-sub/other.marks <input &&
2158+
grep :1 cmdline-sub/other.marks &&
2159+
test_path_is_missing feature-sub
21352160
'
21362161

21372162
test_expect_success 'R: catch typo in marks file name' '
21382163
test_must_fail git fast-import --import-marks=nonexistent.marks </dev/null &&
21392164
echo "feature import-marks=nonexistent.marks" |
2140-
test_must_fail git fast-import
2165+
test_must_fail git fast-import --allow-unsafe-features
21412166
'
21422167

21432168
test_expect_success 'R: import and output marks can be the same file' '
@@ -2193,7 +2218,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
21932218
rm -f io.marks &&
21942219
>expect &&
21952220
2196-
git fast-import --export-marks=io.marks <<-\EOF &&
2221+
git fast-import --export-marks=io.marks \
2222+
--allow-unsafe-features <<-\EOF &&
21972223
feature import-marks-if-exists=not_io.marks
21982224
EOF
21992225
test_cmp expect io.marks &&
@@ -2204,7 +2230,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
22042230
echo ":1 $blob" >expect &&
22052231
echo ":2 $blob" >>expect &&
22062232
2207-
git fast-import --export-marks=io.marks <<-\EOF &&
2233+
git fast-import --export-marks=io.marks \
2234+
--allow-unsafe-features <<-\EOF &&
22082235
feature import-marks-if-exists=io.marks
22092236
blob
22102237
mark :2
@@ -2217,7 +2244,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
22172244
echo ":3 $blob" >>expect &&
22182245
22192246
git fast-import --import-marks=io.marks \
2220-
--export-marks=io.marks <<-\EOF &&
2247+
--export-marks=io.marks \
2248+
--allow-unsafe-features <<-\EOF &&
22212249
feature import-marks-if-exists=not_io.marks
22222250
blob
22232251
mark :3
@@ -2230,7 +2258,8 @@ test_expect_success 'R: feature import-marks-if-exists' '
22302258
>expect &&
22312259
22322260
git fast-import --import-marks-if-exists=not_io.marks \
2233-
--export-marks=io.marks <<-\EOF &&
2261+
--export-marks=io.marks \
2262+
--allow-unsafe-features <<-\EOF &&
22342263
feature import-marks-if-exists=io.marks
22352264
EOF
22362265
test_cmp expect io.marks
@@ -2242,7 +2271,7 @@ test_expect_success 'R: import to output marks works without any content' '
22422271
feature export-marks=marks.new
22432272
EOF
22442273
2245-
cat input | git fast-import &&
2274+
git fast-import --allow-unsafe-features <input &&
22462275
test_cmp marks.out marks.new
22472276
'
22482277

@@ -2252,7 +2281,7 @@ test_expect_success 'R: import marks prefers commandline marks file over the str
22522281
feature export-marks=marks.new
22532282
EOF
22542283
2255-
cat input | git fast-import --import-marks=marks.out &&
2284+
git fast-import --import-marks=marks.out --allow-unsafe-features <input &&
22562285
test_cmp marks.out marks.new
22572286
'
22582287

@@ -2265,7 +2294,8 @@ test_expect_success 'R: multiple --import-marks= should be honoured' '
22652294
22662295
head -n2 marks.out > one.marks &&
22672296
tail -n +3 marks.out > two.marks &&
2268-
git fast-import --import-marks=one.marks --import-marks=two.marks <input &&
2297+
git fast-import --import-marks=one.marks --import-marks=two.marks \
2298+
--allow-unsafe-features <input &&
22692299
test_cmp marks.out combined.marks
22702300
'
22712301

@@ -2278,7 +2308,7 @@ test_expect_success 'R: feature relative-marks should be honoured' '
22782308
22792309
mkdir -p .git/info/fast-import/ &&
22802310
cp marks.new .git/info/fast-import/relative.in &&
2281-
git fast-import <input &&
2311+
git fast-import --allow-unsafe-features <input &&
22822312
test_cmp marks.new .git/info/fast-import/relative.out
22832313
'
22842314

@@ -2290,7 +2320,7 @@ test_expect_success 'R: feature no-relative-marks should be honoured' '
22902320
feature export-marks=non-relative.out
22912321
EOF
22922322
2293-
git fast-import <input &&
2323+
git fast-import --allow-unsafe-features <input &&
22942324
test_cmp marks.new non-relative.out
22952325
'
22962326

@@ -2560,7 +2590,7 @@ test_expect_success 'R: quiet option results in no stats being output' '
25602590
25612591
EOF
25622592
2563-
cat input | git fast-import 2> output &&
2593+
git fast-import 2>output <input &&
25642594
test_must_be_empty output
25652595
'
25662596

transport-helper.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ static int get_importer(struct transport *transport, struct child_process *fasti
431431
child_process_init(fastimport);
432432
fastimport->in = helper->out;
433433
argv_array_push(&fastimport->args, "fast-import");
434+
argv_array_push(&fastimport->args, "--allow-unsafe-features");
434435
argv_array_push(&fastimport->args, debug ? "--stats" : "--quiet");
435436

436437
if (data->bidi_import) {

0 commit comments

Comments
 (0)