LangGraph
Static analysis for LangGraph applications to detect infinite state cycles, unbounded graphs, and missing termination conditions.
Quick Start
inkog scan ./my-langgraph-appWhat Inkog Detects
| Finding | Severity | Description |
|---|---|---|
| Graph Cycle | CRITICAL | StateGraph without exit condition loops forever |
| No Terminal Node | HIGH | Conditional edges without END node |
| Node Timeout | HIGH | Node execution without time limits |
| Unbounded State | HIGH | State accumulation without cleanup |
| Token Bombing | CRITICAL | Cycles that trigger unlimited LLM calls |
StateGraph Infinite Cycles
The most dangerous LangGraph vulnerability. A cycle without proper exit conditions runs indefinitely.
Vulnerable
No exit condition - graph cycles until timeout or crash
from langgraph.graph import StateGraph, END
workflow = StateGraph(AgentState)
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
workflow.add_edge("agent", "tools")
workflow.add_edge("tools", "agent") # Infinite cycle!
workflow.set_entry_point("agent")
# No END edge - graph never terminates
app = workflow.compile()Secure
Conditional edge with iteration limit and completion check
from langgraph.graph import StateGraph, END
workflow = StateGraph(AgentState)
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
def should_continue(state):
if state["iterations"] >= 10:
return "end"
if state["task_complete"]:
return "end"
return "continue"
workflow.add_conditional_edges(
"agent",
should_continue,
{"continue": "tools", "end": END}
)
workflow.add_edge("tools", "agent")
workflow.set_entry_point("agent")
app = workflow.compile()Missing Terminal Nodes
Graphs without END nodes or with unreachable terminals never complete.
Vulnerable
Validate node has no exit path
workflow = StateGraph(State)
workflow.add_node("process", process_node)
workflow.add_node("validate", validate_node)
workflow.add_conditional_edges(
"process",
lambda x: "retry" if x["error"] else "done",
{"retry": "process", "done": "validate"}
)
# validate has no outgoing edge!
# Graph state: stuckSecure
All paths lead to END node
workflow = StateGraph(State)
workflow.add_node("process", process_node)
workflow.add_node("validate", validate_node)
workflow.add_conditional_edges(
"process",
lambda x: "retry" if x["error"] else "done",
{"retry": "process", "done": "validate"}
)
workflow.add_edge("validate", END) # Terminal node
# Or conditional termination
workflow.add_conditional_edges(
"validate",
lambda x: "end" if x["valid"] else "retry",
{"end": END, "retry": "process"}
)Recursion Limits
LangGraph has built-in recursion protection. Always configure it.
Vulnerable
No explicit limit on graph recursion depth
app = workflow.compile()
# Default recursion limit may be too high
# or not enforced in all cases
result = app.invoke(initial_state)Secure
Configured recursion limit and interrupt points
app = workflow.compile()
# Explicit recursion limit
config = {"recursion_limit": 25}
result = app.invoke(
initial_state,
config=config
)
# Or set globally
from langgraph.pregel import Pregel
app = workflow.compile(
checkpointer=memory,
interrupt_before=["human_review"] # Add approval gates
)State Accumulation
Unbounded state growth causes memory issues and context overflow.
Vulnerable
State grows unbounded, exhausting memory
class AgentState(TypedDict):
messages: list # Grows forever
context: str # Accumulates
def agent_node(state):
# Appending without limits
state["messages"].append(new_message)
state["context"] += additional_context
return stateSecure
Bounded state with rolling window
from collections import deque
class AgentState(TypedDict):
messages: Annotated[list, "max_length:20"]
context: str
def agent_node(state):
# Bounded message history
messages = state["messages"][-20:] # Keep last 20
messages.append(new_message)
# Truncate context
context = state["context"]
if len(context) > 4000:
context = summarize(context)
return {"messages": messages, "context": context}Multi-Agent Cycles
Agent handoffs can create infinite loops between agents.
Vulnerable
Agents hand off indefinitely
def router(state):
if "need_research" in state["task"]:
return "researcher"
if "need_writing" in state["task"]:
return "writer"
return "researcher" # Default loops back!
workflow.add_conditional_edges(
"researcher", router, {...}
)
workflow.add_conditional_edges(
"writer", router, {...}
)
# Researcher → Writer → Researcher → ...Secure
Handoff counter with maximum limit
def router(state):
# Track handoff count
handoffs = state.get("handoff_count", 0)
if handoffs >= 3:
return "end"
if state["task_complete"]:
return "end"
if "need_research" in state["task"]:
return "researcher"
return "end" # Default to termination
def increment_handoffs(state):
state["handoff_count"] = state.get("handoff_count", 0) + 1
return state
workflow.add_node("handoff_tracker", increment_handoffs)
workflow.add_conditional_edges(
"handoff_tracker", router,
{"researcher": "researcher", "writer": "writer", "end": END}
)Streaming Without Limits
Streaming responses can accumulate tokens rapidly.
Vulnerable
No limits on streamed events or time
# Streaming all events without limits
for event in app.stream(inputs):
if "messages" in event:
for msg in event["messages"]:
print(msg.content)
# No stop conditionSecure
Event count and time limits
import time
MAX_EVENTS = 100
MAX_TIME = 60
start = time.time()
event_count = 0
for event in app.stream(inputs, config={"recursion_limit": 25}):
event_count += 1
if event_count > MAX_EVENTS:
print("Event limit reached")
break
if time.time() - start > MAX_TIME:
print("Time limit reached")
break
if "messages" in event:
for msg in event["messages"]:
print(msg.content)Best Practices
- Always add
ENDedges - Every path must terminate - Set
recursion_limitin config (recommended: 15-50) - Use
should_continuefunctions for conditional termination - Track iteration count in state as a safety counter
- Bound state size - Truncate messages and context
- Add interrupt points for human-in-the-loop approval
CLI Examples
# Scan LangGraph project
inkog scan ./my-langgraph-app
# Focus on critical issues
inkog scan . -severity critical
# Generate HTML report
inkog scan . -output html > report.htmlRelated
Last updated on