Skip to content

Commit abf702c

Browse files
authored
Detect potential leaks in dart pub publish. (#3049)
Detect potential leaks in `dart pub publish`. Inspired by the paper [How Bad Can It Git?][1] the `LeakDetectionValidator` uses regular expressions and entropy thresholds to scan files for possible secrets about to be published. If files about to be published matches any of the patterns for known secret keys an error is thrown and the publishing event is aborted. To ignore a _false-positive_ a `false_secrets` field is introduced in `pubspec.yaml`. This field is a list of git-ignore style patterns for files to ignore when scanning for potential leaks. This allows package authors to explicitly ignore files known to contain _false positives_. This commit includes API keys and secrets from various systems, these have all been revoked immediately following creation and included for testing and entropy threshold computation. [1]: https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_04B-3_Meli_paper.pdf
1 parent 9941c1f commit abf702c

8 files changed

+868
-4
lines changed

.gitallowed

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Allow file for git-secrets, see:
2+
# https://github.com/awslabs/git-secrets#ignoring-false-positives
3+
4+
# Ignore secrets from leak detection, these secrets have all been revoked and
5+
# are only used for testing the detection logic.
6+
lib/src/validator/leak_detection.dart
7+
test/validator/leak_detection_test.dart

lib/src/ignore.dart

+4-2
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,18 @@ class Ignore {
150150
path.endsWith('/') ? path.substring(0, path.length - 1) : path;
151151
return listFiles(
152152
beneath: pathWithoutSlash,
153-
includeDirs: true,
153+
includeDirs: true, // because we are listing below pathWithoutSlash
154154
listDir: (dir) {
155155
// List the next part of path:
156156
if (dir == pathWithoutSlash) return [];
157157
final startOfNext = dir.isEmpty ? 0 : dir.length + 1;
158158
final nextSlash = path.indexOf('/', startOfNext);
159159
return [path.substring(startOfNext, nextSlash)];
160160
},
161-
ignoreForDir: (dir) => dir == '' ? this : null,
161+
ignoreForDir: (dir) => dir == '.' || dir.isEmpty ? this : null,
162162
isDir: (candidate) =>
163+
candidate == '.' ||
164+
candidate.isEmpty ||
163165
path.length > candidate.length && path[candidate.length] == '/',
164166
).isEmpty;
165167
}

lib/src/pubspec.dart

+38
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,43 @@ class Pubspec {
388388
bool _parsedPublishTo = false;
389389
String _publishTo;
390390

391+
/// The list of patterns covering _false-positive secrets_ in the package.
392+
///
393+
/// This is a list of git-ignore style patterns for files that should be
394+
/// ignored when trying to detect possible leaks of secrets during
395+
/// package publication.
396+
List<String> get falseSecrets {
397+
if (_falseSecrets == null) {
398+
final falseSecrets = <String>[];
399+
400+
// Throws a [PubspecException]
401+
void _falseSecretsError(SourceSpan span) => _error(
402+
'"false_secrets" field must be a list of git-ignore style patterns',
403+
span,
404+
);
405+
406+
final falseSecretsNode = fields.nodes['false_secrets'];
407+
if (falseSecretsNode != null) {
408+
if (falseSecretsNode is YamlList) {
409+
for (final node in falseSecretsNode.nodes) {
410+
final value = node.value;
411+
if (value is! String) {
412+
_falseSecretsError(node.span);
413+
}
414+
falseSecrets.add(value);
415+
}
416+
} else {
417+
_falseSecretsError(falseSecretsNode.span);
418+
}
419+
}
420+
421+
_falseSecrets = List.unmodifiable(falseSecrets);
422+
}
423+
return _falseSecrets;
424+
}
425+
426+
List<String> _falseSecrets;
427+
391428
/// The executables that should be placed on the user's PATH when this
392429
/// package is globally activated.
393430
///
@@ -581,6 +618,7 @@ class Pubspec {
581618
_collectError(() => publishTo);
582619
_collectError(() => features);
583620
_collectError(() => executables);
621+
_collectError(() => falseSecrets);
584622
_collectError(_ensureEnvironment);
585623
return errors;
586624
}

lib/src/validator.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import 'validator/flutter_constraint.dart';
2323
import 'validator/flutter_plugin_format.dart';
2424
import 'validator/gitignore.dart';
2525
import 'validator/language_version.dart';
26+
import 'validator/leak_detection.dart';
2627
import 'validator/license.dart';
2728
import 'validator/name.dart';
2829
import 'validator/null_safety_mixed_mode.dart';
@@ -145,7 +146,8 @@ abstract class Validator {
145146
LanguageVersionValidator(entrypoint),
146147
RelativeVersionNumberingValidator(entrypoint, serverUrl),
147148
NullSafetyMixedModeValidator(entrypoint),
148-
PubspecTypoValidator(entrypoint)
149+
PubspecTypoValidator(entrypoint),
150+
LeakDetectionValidator(entrypoint),
149151
];
150152
if (packageSize != null) {
151153
validators.add(SizeValidator(entrypoint, packageSize));

0 commit comments

Comments
 (0)