Last active
June 30, 2025 14:35
-
-
Save ftes/895d44a929502583803cdbdfd07f0da2 to your computer and use it in GitHub Desktop.
Elixir NimbleCSV prevent CSV injection via credo rule
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.CSV do | |
@moduledoc false | |
NimbleCSV.define(MyyApp.CSV.RFC4180, | |
# defaults from NimbleCSV.Spreadsheet | |
separator: ",", | |
escape: "\"", | |
line_separator: "\r\n", | |
escape_formula: %{["@", "+", "-", "=", "\t", "\r"] => "'"} | |
) | |
NimbleCSV.define(MyApp.CSV.Spreadsheet, | |
# defaults from NimbleCSV.Spreadsheet | |
separator: "\t", | |
escape: "\"", | |
encoding: {:utf16, :little}, | |
trim_bom: true, | |
dump_bom: true, | |
# custom extension | |
escape_formula: %{["@", "+", "-", "=", "\t", "\r"] => "'"} | |
) | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Credo.NoNimbleCSV do | |
@moduledoc false | |
use Credo.Check, | |
base_priority: :high, | |
category: :warning, | |
explanations: [ | |
check: """ | |
To prevent [CSV injection](https://owasp.org/www-community/attacks/CSV_Injection) | |
use `Extensions.NimbleCSV` modules instead of `NimbleCSV`. | |
""" | |
] | |
@forbidden ["NimbleCSV.Spreadsheet", "NimbleCSV.RFC4180"] | |
@doc false | |
def run(%SourceFile{} = source_file, params) do | |
issue_meta = IssueMeta.for(source_file, params) | |
Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) | |
end | |
# Check for any reference to the forbidden module | |
defp traverse({:__aliases__, meta, module_parts} = ast, issues, issue_meta) do | |
module_name = module_parts |> Enum.filter(&is_atom/1) |> Enum.map_join(".", &to_string/1) | |
if module_name in @forbidden do | |
issue = issue_for(issue_meta, meta[:line], module_name) | |
{ast, [issue | issues]} | |
else | |
{ast, issues} | |
end | |
end | |
# Default case - continue traversing | |
defp traverse(ast, issues, _issue_meta) do | |
{ast, issues} | |
end | |
defp issue_for(issue_meta, line_no, module_name) do | |
format_issue( | |
issue_meta, | |
message: "Usage of #{module_name} is not allowed. Use MyApp.CSV.#{module_name} instead.", | |
line_no: line_no, | |
trigger: module_name | |
) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Credo.NoNimbleCSVTest do | |
use Credo.Test.Case, async: true | |
alias MyApp.Credo.NoNimbleCSV | |
test "does NOT report expected code" do | |
""" | |
MyApp.NimbleCSV.RFC4180.dump_to_iodata([]) | |
""" | |
|> to_source_file() | |
|> run_check(NoNimbleCSV) | |
|> refute_issues() | |
end | |
test "reports fully qualified call of RFC4180.dump_to_iodata" do | |
""" | |
NimbleCSV.RFC4180.dump_to_iodata([]) | |
""" | |
|> to_source_file() | |
|> run_check(NoNimbleCSV) | |
|> assert_issue() | |
end | |
test "reports fully qualified call of Spreadsheet.dump_to_iodata" do | |
""" | |
NimbleCSV.Spreadsheet.dump_to_iodata([]) | |
""" | |
|> to_source_file() | |
|> run_check(NoNimbleCSV) | |
|> assert_issue() | |
end | |
test "reports alias of Spreadsheet" do | |
""" | |
alias NimbleCSV.Spreadsheet | |
""" | |
|> to_source_file() | |
|> run_check(NoNimbleCSV) | |
|> assert_issue() | |
end | |
test "does not fail on __MODULE__ alias" do | |
""" | |
alias __MODULE__.Something | |
""" | |
|> to_source_file() | |
|> run_check(NoNimbleCSV) | |
|> refute_issues() | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment