Why write a fix?
Developers have a lot on their plate, and security tools finding issues means even more work for them. Traditional security tools scan (partially) completed code for vulnerabilities. They are slow and provide feedback to the developer after the fact. These tools are considered one of the biggest inhibitors of the developer’s productivity.
Semgrep uses fast analyses that can be easily customized to your organization and project. This ensures that the feedback is much faster, and that the feedback is more relevant, leading to fewer Effective False Positives. A tool like this is more usable and better integrated into the workflows of developers. To improve the developer experience even more, the tool should not just slap developers on the wrist by pointing out their mistakes, but it should actually try to help them remediate these problems. A fix that automatically resolves the problem is the most effective and convenient way to do this.
Applying a fix
Semgrep rules can contain a number of optional fields, one of which is
fix. It provides rule-writers with a simple search-and-replace autofix functionality.
For rules with a
fix field, this fix is shown as a preview when running Semgrep in the command line interface. If you run this example from the Semgrep Playground with the command
semgrep –config use-sys-exit.yaml use-sys-exit.py the output will look like this:
1use-sys-exit 2 Use "sys.exit" over the python shell "exit" built-in. "exit" is a helper for the interactive 3 shell and is not available on all Python implementations. 4 5 ▶▶┆ Autofix ▶ sys.exit(3) 6 9┆ exit(3) 7 ⋮┆---------------------------------------- 8 ▶▶┆ Autofix ▶ sys.exit(4) 9 14┆ exit(4)
To actually apply this fix, and replace the marked code with the code from the
fix field, add the flag
--autofix to your command. Semgrep can also run in CI where the autofix behavior can be used to create pull requests instead.
Writing your own fix
Writing a fix can be very simple, especially since metavariables allow you to reuse parts of the existing code. In the example above, the rule matches any calls of the
exit function with the pattern
$X catches the exit code that is being used. This is very handy, as metavariables can be reused in the
fix field! If we want to replace every call of the
exit function with
sys.exit with the same arguments, the fix looks like this:
Things can get a little more complicated when the pattern matches an unknown number of things. Take the following example, where the goal is to always call
make_transaction with the
secure option set to
1# ruleid: transaction 2make_transaction(transaction = "transaction", secure=False) 3 4# ok: transaction 5make_transaction(transaction = "transaction", secure=True) 6# ok: transaction 7make_transaction(transaction = "transaction", commit=False, secure=True)
Since we don’t know the number of arguments in the call, to match this code, we need ellipsis (
...), resulting in the following rule:
1pattern: make_transaction(..., secure=False)
It is difficult to write a fix for this rule, because we don’t know what is caught by the ellipses. Luckily we have a feature called Ellipsis Metavariables that allows us to name and reuse these ellipses, similar to a regular metavariable. We can then rewrite the rule and add a fix as follows.
1pattern: make_transaction($...ARGS, secure=False) 2fix: make_transaction($...ARGS, secure=True)
Unfortunately, Ellipsis Metavariables are not supported for every language. And if they are supported, they do not cover every construct in that programming language either. Check the docs to see if they cover your required patterns. If they don’t, consider filing an issue on GitHub, so we can add support for your use case.
By using Ellipsis Metavariables we can rewrite unknown pieces of code. Unfortunately, the Ellipsis Metavariables are not supported for all languages.
To get around this, we can combine
pattern-inside. We will use
pattern-inside to match the actual insecure code construct and then use
pattern to match the part in that construct that we actually want to rewrite. In our example, that means matching the
False and rewriting it to
True. The resulting rule and fix look like this:
1 patterns: 2 - pattern-inside: | 3 make_transaction($...BEFORE, secure=False) 4 - pattern: | 5 False 6 fix: | 7 True
This requires some forward thinking for which code construct is insecure and which part of the code we actually want to rewrite. To make this easier, the
focus-metavariable field is added. This allows us to use
pattern-inside however we want, and then put the focus of the fix on a metavariable. Rewriting the rule and fix for our running example results in:
1 patterns: 2 - pattern: | 3 make_transaction($...BEFORE, secure=$VALUE) 4 - metavariable-pattern: 5 metavariable: $VALUE 6 pattern: | 7 False 8 - focus-metavariable: $VALUE 9 fix: | 10 True
Writing fixes can really improve the developer experience of a security tool. With Semgrep we’ve got several tricks up our sleeve to achieve in writing these fixes. These are use of metavariables and ellipsis metavariables, cleverly using a combination of
pattern-inside, and ofcourse the
Semgrep is a fast, open-source, static analysis tool for finding bugs, detecting vulnerabilities in third-party dependencies, and enforcing code standards.