I’ve got a bunch of django_mark_safe errors

>> Issue: [B703:django_mark_safe] Potential XSS on mark_safe function.
   Severity: Medium   Confidence: High
   Location: ...
   More Info: https://bandit.readthedocs.io/en/latest/plugins/b703_django_mark_safe.html
54 return mark_safe(f'<a href="https://stackoverflow.com/questions/52596576/{url}" target="_blank">{title}</a>')

>> Issue: [B308:blacklist] Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed.
   Severity: Medium   Confidence: High
   Location: ...
   More Info: https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b308-mark-safe
54 return mark_safe(f'<a href="https://stackoverflow.com/questions/52596576/{url}" target="_blank">{title}</a>')

And I’m curious if there is a way to skip or ignore such lines? I understand that using mark_safe could be dangerous, but what if I want to take the risk? For example this method is the only way to display custom link in Django admin, so I don’t know any other option how to do it without mark_safe

I’ve got an answer here:

Two ways:

  1. You can skip the B703 and B308 using the –skip argument to the
    command line.
  2. Or you can affix a comment # nosec on the line to skip.

https://bandit.readthedocs.io/en/latest/config.html#exclusions

Heads up for annotating multilines with # nosec:

given:

li_without_nosec = [
    "select * from %s where 1 = 1 "
    % "foo"
]

li_nosec_at_start_works = [  # nosec - ? and you can put a comment
    "select * from %s where 1 = 1 "
    % "foo"
]  

# nosec - there's an enhancement request to marker above line
li_nosec_on_top_doesntwork = [  
    "select * from %s where 1 = 1 "
    % "foo"
]  

li_nosec_at_end_doesntwork = [
    "select * from %s where 1 = 1 "
    % "foo"
]  # nosec 

output:

>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
   Severity: Medium   Confidence: Low
   Location: test.py:3
   More Info: https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html
2   li_without_nosec = [
3       "select * from %s where 1 = 1 "
4       % "foo"
5   ]

--------------------------------------------------
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
   Severity: Medium   Confidence: Low
   Location: test.py:15
   More Info: https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html
14  li_nosec_on_top_doesntwork = [
15      "select * from %s where 1 = 1 "
16      % "foo"
17  ]

--------------------------------------------------
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
   Severity: Medium   Confidence: Low
   Location: test.py:21
   More Info: https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html
20  li_nosec_at_end_doesntwork = [
21      "select * from %s where 1 = 1 "
22      % "foo"
23  ]  # nosec

Black

Here’s hoping that black won’t get involved and restructure the lines, moving the # nosec around.

so much for hope… black does move things around, just like it does with pylint directives, whenever the line length becomes too long. At which point # nosec ends up at the end.

You can either proactively break up the line and position # nosec at the first one. Or you can just wait out black and adjust if needed.

Just to complete the topic – in my case I had to rid of B322: input rule, and didn’t wanted to write # nosec each time I found this problem in the code, or to always execute Bandit with a --skip flag.

So if you want to omit a certain rule for whole solution, you can create a .bandit file in the root of your project. Then you can write which rules should be skipped every time, for example:

[bandit]
skips: B322

And then Bandit will skip this check by default without need to give additional comments in the code.

You can config Bandit with .bandit INI file (only if it is invoked with -r option):

[bandit]
tests = B101,B102,B301

Or with pyproject.toml file:

[tool.bandit]
tests = ["B201", "B301"]
skips = ["B101", "B601"]

Or with yaml file:

skips: ['B101', 'B601']
assert_used:
  skips: ["*/test_*.py", "*/test_*.py"]

See https://bandit.readthedocs.io/en/latest/config.html