-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Support an extensible avoid_unsafe_apis lint in analyzer #40595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I generally like this proposal, but I do have a few comments / suggestions. We should consider making this a hint (enabled by default) rather than a lint (disabled by default). The reason I say that is because the hint wouldn't do anything unless the user has also specified a list of files containing descriptions of unsafe APIs, so providing that list is already a way of opting in, and it seems better to not require the user to opt in multiple times. Not a big issue internally because we'll have a small number of people that control the analysis options files, but it would improve the usability externally. As for the list of files, relative file paths would work fine internally because everyone has exactly the same layout on disk, but externally that isn't the case. I'd like to propose that we use URIs. This would make this feature easier to use externally and would be consistent with the way the If we do use a hint, then we'll need a different place to add the list of unsafe APIs. I'd like to propose that we use something like the following:
Of course, relative paths would still work as a valid URI, just like in import statements, but this provides more flexability. I added the extra The class Using symbols for the APIs that are unsafe is definitely better than using strings, but it might be better to use an arbitrary expression. The only compile-time checking done for symbols is to ensure that they are syntactically valid. In particular, there's no association made between
This is safe because the code will never be executed. The one case I'm not thrilled with is setters, but I think we can live with it. Note that I also changed the example from
@matanlurey Curious as to whether this would (or could be made to) address your needs. I'd prefer to have a single solution if possible. |
I don't have time to go into a lot of detail with OOO coming up, but in summary, this might work for our needs as written, but I am not confident:
The best person to discuss this with would be @srawlins who is currently OOO. |
I believe that this would work for APIs in the SDK by doing something like the following:
The analyzer would then flag any use of
That's a bit trickier, but I can think of a couple of possibilities. I'll talk to Sam when he's back to work out the details. |
Thoughtful write-up! We've talked about opening lints up to configuration (proposed for example in #57673). If we supported that, could this be simplified to look something like this? linter:
rules:
unsafe_apis:
api:
- element: 'package:mypackage/MyClass#myMethod'
- description: 'Unsafe method. Consider using other method'
api:
- element: 'package:mypackage/MyClass#myVar'
- description: 'Setting {0} is not safe.' or, taking it further: linter:
rules:
unsafe_apis:
element:
- kind: method_invocation
- name: 'package:mypackage/MyClass#myMethod'
- description: 'Invoking method {0} is unsafe. Use OtherClass#foo instead.' |
Yes. One downside is that the information is now buried in strings, so refactorings, "find references", etc. won't work with them (at least not without additional work). Another is that all of these unsafe APIs would need to be specified in a single file, and there is a goal of allowing them to be split up. I don't know how important that goal will be in practice. |
Re: @matanlurey's comment on whitelisting... I was imagining all grandfathered usage would need to be annotated in code with an +1 to all of @bwilkerson's comments. I would very much likely avoid a stringified representation of these APIs for the reasons Brian mentions. I agree that the multiple configuration files use case is weak. It primarily existed because if we do have real symbols/references in this file, a single unsafe_apis.dart file would end up having 100s of dependencies in a large centralized repository like google3. However, this file can be generated in google3 from smaller |
There is a potentially interesting external case for allowing multiple URLs to be specified: if individual package providers wanted to provide a file specific to the APIs exposed by their package. I don't know how likely that is to happen, but I'd want any design to not preclude future support for multiple URLs (assuming that we support a single URL initially). |
I don't think generating smaller files in google3 is sufficient to solve the dependency problem; I think we'll need to at least allow (not require) stringified config. Otherwise, we force you to take a dependency on a library in order to ensure you don't use it. This is certain to bite us at some point. (Your code wouldn't depend on the library, but your build would, in order to run analysis; that means any change to the library you're not supposed to use will force your build to rerun / trigger all your tests on the continuous build.) |
This lint should only be used for cases where you are banning a subset of APIs exposed by a library that is already being used by the app. In google3, we would have a centralized This lint should not be used for cases where you are banning an entire library like 1: See FAQ in the proposal. |
For declaring unsafeness in a library, I'd very much recommend using an annotation, not a normal declaration. The normal declaration introduces a name that can be accessed by arbitrary code and retained at run-time, which seems unnecessary for something that is only intended for the analyzer. For: import 'dart:html';
var unsafeApis = [
Unsafe(Element.appendHtml, "Don't append raw HTML."),
]; I would write @Unsafe(#Element.appendHtml, "Don't append raw HTML")
import 'dart:html'; and for import ‘package:my_package/my_package.dart’;
var unsafeFlutterApis = [
Unsafe(MyClass.myMethod, ‘Unsafe method. Consider using other method’, ignorable: false), // method
Unsafe(MyClass.someValue, ‘Unsafe value. Consider using other value.’), // getter
Unsafe(MyClass.someValue = throw 0, ‘Setting this is not safe.’), // setter
]; I would do @Unsafe(#MyClass.myMethod, ‘Unsafe method. Consider using other method’, ignorable: false), // method
@Unsafe(#MyClass.someValue, ‘Unsafe value. Consider using other value.’), // getter
@Unsafe(#MyClass.someValue=, ‘Setting this is not safe.’), // setter
library something.something; (I really want to allow a In both cases you cannot refer to instance methods like (I'd also love to have We should consider some way of marking annotations as "not retained at run-time" (like the Java retention policy annotation). |
True. But I think the danger that arbitrary code would attempt to access these objects is fairly low. Still, I suppose it might be considered a security hole to have code that could be used at runtime to determine which APIs are not safe to use. We could get around the latter by making the top-level variable final, or even by putting the constructor invocations in a private top-level function.
Not true. The analyzer wouldn't need to evaluate anything in order to get the information it needs. All of the information is available through static analysis without any extra work. Using annotations wouldn't be any better from an implementation point of view.
You are, of course, right. I missed that. In which case I suppose we have no choice by to use symbols.
True. Not that it matters because that syntax won't work for instance members, but I was suggesting using it as an argument to a non-const constructor, in which case it wouldn't be a constant expression.
We don't necessarily need new syntax for that purpose. We could define a lint that would catch the use of symbols that do not correspond to existing declarations. We could also special case these "unsafe api" files to require that all symbols reference existing declarations and even support them in our rename refactoring. |
My main worry about using program-level declarations for metadata is not security, just style. It is metadata, so it should be represented as metadata, which in Dart means annotations. At least unless there is a very compelling reason to do something else, which there does not seem to be here. It's cool that the analyzer can handle non-constant expressions without extra effort, but if any other source-tool needs access to the information, it would still be better if it was stored as constant annotations like other metadata. It does make it constant expressions, which comes with some restrictions, but probably not something which is insurmountable. As for |
Thanks for pushing this forward. While we discuss the proper way to do this, I will introduce a google3-only lint that uses string representation to ban a set of APIs from all Flutter apps. This will give us the option to move quickly and assess how practical is it to define these at library level. It will also allow us to ban these APIs sooner than later as several security reviews are flagging these as critical fixes. We would like to avoid apps being blocked while we wait for the long-term implementation of the The only request I have from you is an incremental plan as I believe the temporary solution I am describing above is error-prone and not scalable. Ideally, we should have support for this by mid-Q2. Do you think that's reasonable? |
Of whom are you asking that question? |
Given the changes required in analyzer, I am not the right person to work on this as I am not familiar with the internals of the analyzer and unfortunately, I don't have the bandwidth to become familiar in the foreseeable future. I am happy to guide the effort from the requirements perspective. I was hoping @davidmorgan or your team can pick up the implementation, Brian. Let me know if that is unreasonable so we can figure out a different solution. |
@devoncarew @stereotype441 For resourcing discussions. |
See http://go/flutter-lints-dd for overall context. Also see #40429 for a separate attempt to add a generic
Unsafe
annotation which is how this proposal sprung to life.tl;dr
We would like to define a new lint
avoid_unsafe_apis
where the definition of unsafe APIs is domain specific and can be passed in as input to the analyzer.Proposal
avoid_unsafe_apis lint would read some configuration that would contain a list of symbols and the rationale for considering them banned. We first define a class to represent Unsafe APIs and the rationale.
Then the application can define a Dart file that contains the list of APIs it considers unsafe:
In
analysis_options.yaml
file, annotate the avoid_unsafe_apis lint with a list of files that contain these banned apis:Analyzer reads this configuration and finds the union of all unsafe APIs listed in these files. The lint itself simply walks through various expression types and flags if the statement matches to any of the unsafe symbols.
Currently the following constructs can be marked as unsafe: Top-level variables, functions, getters, setters, constructors, static and instance members of classes, enums, and enum constants. We do not want to be able to mark entire Classes or Libraries as unsafe.
FAQ
Why Dart instead of json or yaml?
We can eventually make it easier and less error prone to refer to Dart symbols in a Dart file. You can imagine an evolution of Unsafe class as follows:
and the corresponding unsafe_apis.dart file:
This way, we can ensure the correctness of the symbols without having to write another set of tests to map string representations to actual symbols.
I would love it if we could get to this in v1.
Why support multiple files?
For very large projects, a single file might get too unwieldy. Developers would prefer to split these perhaps by package. Internally, we would like each
unsafe_apis.dart
to live in its own package directory rather than in the application. Analyzer will be provided with the full path of these files.Why not mark Classes and Libraries as unsafe?
Large containers of code are hard to mark as unsafe. As they get larger (class => library), it takes a lot of discipline to stick to the encapsulation. For instance, a File object might offer write method which can be considered unsafe but also absolutePath getter which might not be. If someone argues that File itself is unsafe, all of a sudden absolutePath needs to find a new place to be used safely. We would want to avoid creation of classes like UnsafeFile and SafeFile and instead rely on library maintainers marking the smallest unit of compilation that deserves the unsafe annotation.
We also don't want to mark large containers as unsafe because the only mechanism to allow an unsafe API is via
//ignore
comment. This forces the developer to give a valid reason for every instance of unsafe API usage (including links to a security review). If we were to mark entire libraries as unsafe, you will end up seeing unintended effects such as:See #40429 for further discussion.
The text was updated successfully, but these errors were encountered: