| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| @TestOn('vm') | |
| library; | |
| import 'dart:io'; | |
| import 'package:path/path.dart' as p; | |
| import 'package:pub/src/exit_codes.dart' as exit_codes; | |
| import 'package:pub/src/git.dart' as git; | |
| import 'package:pub/src/source/git.dart'; | |
| import 'package:test/test.dart'; | |
| import '../../descriptor.dart' as d; | |
| import '../../test_pub.dart'; | |
| void main() { | |
| test('rejects git dependency with ext:: protocol in pubspec', () async { | |
| ensureGit(); | |
| await git.run(['config', '--global', 'protocol.ext.allow', 'user']); | |
| addTearDown(() async { | |
| await git.run(['config', '--global', '--unset', 'protocol.ext.allow']); | |
| }); | |
| final proofFile = p.join(d.sandbox, 'pwned.txt'); | |
| await d | |
| .appDir( | |
| dependencies: { | |
| 'foo': {'git': 'ext::sh -c touch\$IFS$proofFile @'}, | |
| }, | |
| ) | |
| .create(); | |
| await pubGet( | |
| error: contains('is not a valid Git URL'), | |
| exitCode: exit_codes.DATA, | |
| ); | |
| expect(File(proofFile).existsSync(), isFalse); | |
| }); | |
| test('rejects git dependency starting with a hyphen in pubspec', () async { | |
| await d | |
| .appDir( | |
| dependencies: { | |
| 'foo': {'git': '--upload-pack=evil'}, | |
| }, | |
| ) | |
| .create(); | |
| await pubGet( | |
| error: contains('is not a valid Git URL'), | |
| exitCode: exit_codes.DATA, | |
| ); | |
| }); | |
| test('git clone passes -- to prevent option injection', () async { | |
| ensureGit(); | |
| final tempDir = p.join(d.sandbox, 'temp_clone'); | |
| // With '--', Git treats '--upload-pack=...' as a repository name rather | |
| // than a CLI option. | |
| expect( | |
| () => git.run([ | |
| 'clone', | |
| '--mirror', | |
| '--', | |
| '--upload-pack=touch /tmp/evil', | |
| tempDir, | |
| ]), | |
| throwsA( | |
| isA<git.GitException>().having( | |
| (e) => e.stderr, | |
| 'stderr', | |
| isNot(contains('unknown option')), | |
| ), | |
| ), | |
| ); | |
| }); | |
| test('GitDescription validates URL schemes and syntax', () { | |
| expect( | |
| () => GitDescription( | |
| url: '-u/payload', | |
| ref: 'main', | |
| path: '.', | |
| containingDir: null, | |
| tagPattern: null, | |
| ), | |
| throwsA(isA<FormatException>()), | |
| ); | |
| expect( | |
| () => GitDescription( | |
| url: 'ext::sh -c evil @', | |
| ref: 'main', | |
| path: '.', | |
| containingDir: null, | |
| tagPattern: null, | |
| ), | |
| throwsA(isA<FormatException>()), | |
| ); | |
| expect( | |
| () => GitDescription( | |
| url: 'ext:///evil', | |
| ref: 'main', | |
| path: '.', | |
| containingDir: null, | |
| tagPattern: null, | |
| ), | |
| throwsA(isA<FormatException>()), | |
| ); | |
| expect( | |
| () => GitDescription( | |
| url: 'https://example.com/@flutter/plugins.git', | |
| ref: 'main', | |
| path: '.', | |
| containingDir: null, | |
| tagPattern: null, | |
| ), | |
| returnsNormally, | |
| ); | |
| expect( | |
| () => GitDescription( | |
| url: 'ssh://git@example.com/flutter/plugins.git', | |
| ref: 'main', | |
| path: '.', | |
| containingDir: null, | |
| tagPattern: null, | |
| ), | |
| returnsNormally, | |
| ); | |
| expect( | |
| () => GitDescription( | |
| url: 'git@example.com:flutter/plugins.git', | |
| ref: 'main', | |
| path: '.', | |
| containingDir: null, | |
| tagPattern: null, | |
| ), | |
| returnsNormally, | |
| ); | |
| }); | |
| } |