Packages
metastatic
0.14.2
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/analysis/business_logic/insecure_direct_object_reference.ex
defmodule Metastatic.Analysis.BusinessLogic.InsecureDirectObjectReference do
@moduledoc """
Detects Insecure Direct Object Reference (IDOR) vulnerabilities (CWE-639).
This analyzer identifies code patterns where user-supplied IDs are used to
directly access resources without verifying ownership or authorization.
## Cross-Language Applicability
IDOR is a **universal access control vulnerability**:
- **Elixir**: `Repo.get!(Post, params["id"])` without ownership check
- **Python**: `Post.objects.get(id=request.GET['id'])`
- **JavaScript**: `Post.findById(req.params.id)`
- **Ruby**: `Post.find(params[:id])`
- **Java**: `postRepository.findById(request.getParameter("id"))`
- **C#**: `_context.Posts.Find(id)`
- **Go**: `db.First(&post, id)`
## Problem
When resources are accessed directly by ID without authorization:
- Users can access other users' data by guessing/incrementing IDs
- Horizontal privilege escalation is trivial
- Data theft and privacy violations
## Detection Strategy
Detects patterns where:
1. Resources are fetched by user-supplied ID
2. No ownership check (resource.user_id == current_user.id) is apparent
3. No authorization policy is applied
"""
@behaviour Metastatic.Analysis.Analyzer
alias Metastatic.Analysis.Analyzer
@fetch_functions ~w[
get get! get_by find find! find_by
findById findByPk findOne first
retrieve load fetch lookup
Repo.get Repo.get! Repo.get_by
objects.get objects.filter
]
@ownership_indicators ~w[
user_id owner_id created_by author_id
belongs_to current_user owner
where user: filter user
scope user policy authorize
]
@impl true
def info do
%{
name: :insecure_direct_object_reference,
category: :security,
description:
"Detects IDOR vulnerabilities - direct object access without ownership check (CWE-639)",
severity: :warning,
explanation: """
Insecure Direct Object Reference (IDOR) occurs when user-supplied IDs are used
to directly access resources without verifying the user owns or can access them.
This allows attackers to:
- Access other users' data by changing ID values
- Modify or delete resources they don't own
- Enumerate resources through sequential IDs
Always verify authorization:
- Check resource ownership (resource.user_id == current_user.id)
- Use scoped queries (where(user_id: current_user.id))
- Apply authorization policies before returning data
""",
configurable: true
}
end
@impl true
# Detect fetch operations with user-supplied IDs
def analyze({:function_call, meta, args} = node, context) when is_list(meta) do
func_name = Keyword.get(meta, :name, "")
if fetch_function?(func_name) and has_user_supplied_id?(args) and
not in_authorization_context?(context) do
[
Analyzer.issue(
analyzer: __MODULE__,
category: :security,
severity: :warning,
message: "Potential IDOR: '#{func_name}' with user-supplied ID without ownership check",
node: node,
metadata: %{
cwe: 639,
function: func_name,
suggestion: "Add ownership check or use scoped query (where user_id: current_user.id)"
}
)
]
else
[]
end
end
def analyze(_node, _context), do: []
# ----- Private Helpers -----
defp fetch_function?(func_name) when is_binary(func_name) do
func_lower = String.downcase(func_name)
Enum.any?(@fetch_functions, fn pattern ->
String.contains?(func_lower, String.downcase(pattern))
end)
end
defp fetch_function?(_), do: false
defp has_user_supplied_id?(args) when is_list(args) do
Enum.any?(args, fn arg ->
case arg do
{:variable, _meta, name} when is_binary(name) ->
name_lower = String.downcase(name)
String.contains?(name_lower, "id") or String.contains?(name_lower, "param")
{:map_access, _meta, _} ->
true
{:attribute_access, _meta, _} ->
true
_ ->
false
end
end)
end
defp has_user_supplied_id?(_), do: false
defp in_authorization_context?(context) do
parent_stack = Map.get(context, :parent_stack, [])
Enum.any?(parent_stack, &has_ownership_check?/1)
end
defp has_ownership_check?(node) do
case node do
{:conditional, _meta, [condition | _]} ->
involves_ownership?(condition)
{:function_call, meta, _args} when is_list(meta) ->
func_name = Keyword.get(meta, :name, "")
func_lower = String.downcase(func_name)
Enum.any?(@ownership_indicators, &String.contains?(func_lower, &1))
{:binary_op, meta, [left, right]} when is_list(meta) ->
operator = Keyword.get(meta, :operator)
if operator in [:==, :===] do
involves_ownership?(left) or involves_ownership?(right)
else
false
end
_ ->
false
end
end
defp involves_ownership?(node) do
case node do
{:attribute_access, _meta, children} when is_list(children) ->
Enum.any?(children, fn
{:literal, _, attr} when is_binary(attr) or is_atom(attr) ->
attr_lower = to_string(attr) |> String.downcase()
Enum.any?(@ownership_indicators, &String.contains?(attr_lower, &1))
_ ->
false
end)
{:variable, _meta, name} when is_binary(name) ->
name_lower = String.downcase(name)
Enum.any?(@ownership_indicators, &String.contains?(name_lower, &1))
{:function_call, meta, _} when is_list(meta) ->
func_name = Keyword.get(meta, :name, "")
func_lower = String.downcase(func_name)
Enum.any?(@ownership_indicators, &String.contains?(func_lower, &1))
_ ->
false
end
end
end