sensitive 1.0.0
sensitive: ^1.0.0 copied to clipboard
Analyzer plugin that keeps secrets out of logs. A value annotated with @Sensitive must not be interpolated, stringified, logged, or put into an exception message.
sensitive #
Keeps secrets out of logs, delivered as an analyzer plugin, so the check
runs in your IDE and in dart analyze / flutter analyze with no extra
tooling.
A secret leaks the moment it is written somewhere readable, and the usual way that happens is a debug string nobody meant to keep. Mark the value once, and the analyzer objects wherever it would escape into text.
| Rule | What it catches |
|---|---|
sensitive_exposure |
A value marked @Sensitive() that is interpolated, stringified, logged or put into an exception message. |
Requires Dart 3.10 or later (analyzer plugins are not supported before that).
Installation #
The package ships both the annotation and the rule, so it is listed twice:
once as a dependency (you write @Sensitive in your code) and once as a
plugin (the analyzer runs the rule).
# pubspec.yaml
dependencies:
sensitive: ^1.0.0
# analysis_options.yaml
plugins:
sensitive: ^1.0.0
The rule is registered as a warning rule, so it is active as soon as the
plugin is enabled. Restart the Dart Analysis Server after changing the
plugins section.
sensitive_exposure #
A secret leaks the moment it is written somewhere readable, and the usual way that happens is a debug string nobody meant to keep:
class Session {
@Sensitive('OAuth refresh token')
final String token;
Session(this.token);
// reported: the token would land in the log.
void debug() => print('token: $token');
// reported: the token would land in the crash report.
void check() {
if (token.isEmpty) throw StateError('empty token: $token');
}
}
Annotate the field, getter, parameter, local or top-level variable that holds
the secret with @Sensitive(), optionally with a reason that is shown in the
message. The rule then reports a reference to it in any of four places:
- string interpolation,
'... $token', and'...' + tokenconcatenation; - an explicit
token.toString(); - an argument of a call whose name looks like a logging sink (
print,debugPrint,log,info,warning,severe,write,addErrorand friends), on any receiver, so third-party loggers are covered; - an argument of a constructor of a type that implements
Exceptionor extendsError.
Everything else is left alone, so the secret can still be compared, hashed, sent over the wire, or stored:
bool matches(String input) => token == input; // fine
void log() => print('token length: ${token.length}'); // fine
A @Sensitive field reached through a local alias or a conditional is still
reported, and the annotation on a field also covers the this.token
constructor parameter that initializes it.
Turning the rule off #
Disable it for the whole package:
plugins:
sensitive:
diagnostics:
sensitive_exposure: false
Suppress one diagnostic with a comment, prefixed by the plugin name. The comment applies to the line below it:
// ignore: sensitive/sensitive_exposure
print('token: $token');
// ignore_for_file: sensitive/sensitive_exposure works as well.
Known limits #
These are deliberate boundaries, not bugs:
- Logging sinks are recognised by name. A sink called something else
(
report,emit) is not covered, and a non-logging method that happens to be calledwriteis reported. The list is deliberately wide, because loggers live in other packages (logging,logger,talker, a hand-rolledLogclass) and a secret reaching any of them is a leak regardless of which one it is. - Only direct references are tracked. A secret copied into another field, returned from a getter, or wrapped in a record first is no longer followed. Aliases are followed within a single function body, and only when the local is never reassigned.
- There is no quick fix, and there should not be: redacting a secret is a judgement about what may safely be shown, and no mechanical rewrite can make it.
Example #
An example package is wired to this one by path, and is the end-to-end check that the plugin loads and fires:
cd example
dart pub get
dart analyze lib/example.dart
It deliberately contains violations, so dart analyze exits non-zero there.
Use dart analyze, not flutter analyze: the Flutter wrapper runs its own
bundled analysis and drops diagnostics that come from a third-party analyzer
plugin.
Development #
dart analyze --fatal-infos # must be clean
dart test # the rule matrix plus plugin registration
dart run tool/verify_example.dart # the plugin loads, and fires where documented
That last one matters more than it looks. dart test drives the rule directly
through the analyzer's testing harness, which never loads the plugin the way the
analysis server does, so a plugin that fails to start leaves every test passing
while reporting nothing at all for a real user. The example is the only place the
whole path runs, and tool/verify_example.dart holds it to its own
// reported: comments.
See CONTRIBUTING.md for the scope of the rule, the commit conventions, and how a release is cut.
License #
MIT. See LICENSE.