security: fix critical vulnerabilities and atom exhaustion risks
Critical fixes: - Add [:safe] option to binary_to_term to prevent RCE attacks - Implement whitelist validation for String.to_atom conversions - Add input validation before String.to_existing_atom usage Changes: - MIB compiler and cache: Use safe binary deserialization - SNMP contexts: Whitelist protocol, device type, and source atoms - API controllers: Validate error message keys before atom conversion - Reduce function nesting to comply with Credo standards All 6,145 tests passing with zero Credo issues. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
24236dbe30
commit
fb1d4c564f
7 changed files with 62 additions and 9 deletions
|
|
@ -584,9 +584,11 @@ defmodule SnmpKit.SnmpLib.Cache do
|
|||
end
|
||||
|
||||
defp decompress_value(compressed_value) do
|
||||
# Use safe: true to prevent arbitrary code execution via malicious cached terms
|
||||
# sobelow_skip ["Misc.BinToTerm"]
|
||||
compressed_value
|
||||
|> :zlib.uncompress()
|
||||
|> :erlang.binary_to_term()
|
||||
|> :erlang.binary_to_term([:safe])
|
||||
end
|
||||
|
||||
# Pattern matching and invalidation
|
||||
|
|
|
|||
|
|
@ -200,7 +200,9 @@ defmodule SnmpKit.SnmpLib.MIB.Compiler do
|
|||
def load_compiled(compiled_path) do
|
||||
case File.read(compiled_path) do
|
||||
{:ok, binary_data} ->
|
||||
case :erlang.binary_to_term(binary_data) do
|
||||
# Use safe: true to prevent arbitrary code execution via malicious terms
|
||||
# sobelow_skip ["Misc.BinToTerm"]
|
||||
case :erlang.binary_to_term(binary_data, [:safe]) do
|
||||
%{__type__: :compiled_mib} = compiled ->
|
||||
{:ok, compiled}
|
||||
|
||||
|
|
|
|||
|
|
@ -302,10 +302,17 @@ defmodule Towerops.Devices do
|
|||
community: device.snmp_community,
|
||||
port: device.snmp_port || 161,
|
||||
transport: device.snmp_transport || "udp",
|
||||
source: String.to_atom(device.snmp_community_source || "organization")
|
||||
source: safe_source_to_atom(device.snmp_community_source)
|
||||
}
|
||||
end
|
||||
|
||||
# Safely convert credential source to atom with whitelist to prevent atom exhaustion
|
||||
defp safe_source_to_atom("device"), do: :device
|
||||
defp safe_source_to_atom("site"), do: :site
|
||||
defp safe_source_to_atom("organization"), do: :organization
|
||||
defp safe_source_to_atom(nil), do: :organization
|
||||
defp safe_source_to_atom(_unknown), do: :organization
|
||||
|
||||
@doc """
|
||||
Gets MikroTik API configuration for a device with hierarchical fallback.
|
||||
|
||||
|
|
|
|||
|
|
@ -1245,7 +1245,7 @@ defmodule Towerops.Snmp do
|
|||
neighbor_protocols =
|
||||
neighbors
|
||||
|> Enum.map(& &1.protocol)
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
|> Enum.map(&safe_protocol_to_atom/1)
|
||||
|> Enum.uniq()
|
||||
|
||||
protocols ++ neighbor_protocols
|
||||
|
|
@ -1257,6 +1257,11 @@ defmodule Towerops.Snmp do
|
|||
Enum.uniq(protocols)
|
||||
end
|
||||
|
||||
# Safely convert protocol string to atom with whitelist to prevent atom exhaustion
|
||||
defp safe_protocol_to_atom("lldp"), do: :lldp
|
||||
defp safe_protocol_to_atom("cdp"), do: :cdp
|
||||
defp safe_protocol_to_atom(_unknown), do: :unknown
|
||||
|
||||
# VLAN queries
|
||||
|
||||
@doc """
|
||||
|
|
@ -1905,9 +1910,16 @@ defmodule Towerops.Snmp do
|
|||
|> Enum.sort_by(& &1.device_count, :desc)
|
||||
end
|
||||
|
||||
# Convert device type to atom (handles both string and atom)
|
||||
# Convert device type to atom with whitelist to prevent atom exhaustion
|
||||
defp device_type_atom(device) when is_atom(device), do: device
|
||||
defp device_type_atom(device) when is_binary(device), do: String.to_atom(device)
|
||||
defp device_type_atom("router"), do: :router
|
||||
defp device_type_atom("switch"), do: :switch
|
||||
defp device_type_atom("wireless"), do: :wireless
|
||||
defp device_type_atom("server"), do: :server
|
||||
defp device_type_atom("workstation"), do: :workstation
|
||||
defp device_type_atom("firewall"), do: :firewall
|
||||
defp device_type_atom("printer"), do: :printer
|
||||
defp device_type_atom("phone"), do: :phone
|
||||
defp device_type_atom(_), do: :unknown
|
||||
|
||||
# Calculate CIDR notation from IP and mask
|
||||
|
|
|
|||
|
|
@ -177,8 +177,18 @@ defmodule ToweropsWeb.Api.MobileAuthController do
|
|||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
safe_translate_key(key, opts)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
# Safely translate error message keys to prevent atom exhaustion
|
||||
defp safe_translate_key(key, opts) do
|
||||
# Validate key length and format before converting to atom to prevent abuse
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -333,8 +333,18 @@ defmodule ToweropsWeb.Api.V1.DevicesController do
|
|||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
safe_translate_key(key, opts)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
# Safely translate error message keys to prevent atom exhaustion
|
||||
defp safe_translate_key(key, opts) do
|
||||
# Validate key length and format before converting to atom to prevent abuse
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -221,8 +221,18 @@ defmodule ToweropsWeb.Api.V1.SitesController do
|
|||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
safe_translate_key(key, opts)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
# Safely translate error message keys to prevent atom exhaustion
|
||||
defp safe_translate_key(key, opts) do
|
||||
# Validate key length and format before converting to atom to prevent abuse
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue