SecureRandom inside Workflow
- Engine
- Temporal Java SDK
- Since
- 0.3.0
- Auto Fix
- Not Available
- Category Range
- WG001–WG099
Description
A Temporal workflow implementation — or any code reachable from a workflow's entry
point, however many method calls away — constructs a java.security.SecureRandom (new SecureRandom()) or draws a value from one (nextBytes(), nextInt()).
Why This Matters
SecureRandom draws cryptographically strong randomness from a source that is not
recorded in workflow history. Temporal workflows are replayed: whenever a worker needs
to reconstruct a workflow's state, it re-executes the workflow's code from the beginning
of event history. Replay re-executes the construction (or the draw), producing a
different sequence of values than the original execution saw. If any decision in the
workflow depends on that value, replay diverges from the original execution and raises a
NonDeterministicException.
io.temporal.workflow.Workflow.newRandom() is the correct replacement for
workflow-internal randomness: it returns a Random seeded deterministically from
workflow history, so it produces the same sequence of values on every replay. If genuine
cryptographic randomness is required, generate it inside an Activity instead.
Violation Example
import java.security.SecureRandom;
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() {
SecureRandom random = new SecureRandom();
byte[] token = new byte[16];
random.nextBytes(token);
}
}This is flagged even when the call is several methods away from the workflow entry point:
public class PaymentWorkflowImpl implements PaymentWorkflow {
private final PaymentService paymentService = new PaymentService();
@Override
public void processPayment() {
// WG007 still fires: WoGu's call-graph analysis follows this call into
// PaymentService.generateToken() and finds `new SecureRandom()` inside it.
paymentService.generateToken();
}
}
class PaymentService {
byte[] generateToken() {
byte[] token = new byte[16];
new SecureRandom().nextBytes(token);
return token;
}
}Constructing the SecureRandom and calling an instance method on it are each their own
violation, so the example above is reported twice: once for new SecureRandom(), once
for the SecureRandom.nextBytes() call.
Compliant Example
import io.temporal.workflow.Workflow;
public class PaymentWorkflowImpl implements PaymentWorkflow {
@Override
public void processPayment() {
byte[] token = new byte[16];
Workflow.newRandom().nextBytes(token);
}
}Recommended Fix
Replace new SecureRandom() (and the draws from it) with Workflow.newRandom() (or
another Temporal-provided deterministic alternative). It is seeded from workflow history,
so it produces the same sequence of values on every replay. If cryptographic randomness
is genuinely required, generate it inside an Activity instead — activities are not
replayed, so SecureRandom is safe there. WG007 only flags code reachable from a
workflow's own entry point, and stops before entering an Activity implementation (see
False Positives).
False Positives
WG007 uses the same call-graph analysis as WG001–WG006, extended
to also look for matching constructor calls (new SecureRandom()) alongside matching
method calls, at every call site reachable from a workflow's entry-point method(s).
An instance call like random.nextBytes() doesn't name SecureRandom anywhere at the
call site, so it can't be matched syntactically the way a static call can; WG007 instead
resolves the call via WoGu's symbol solver and checks that it declares to
java.security.SecureRandom itself. Note that SecureRandom extends java.util.Random
but does not override nextInt(), so a secureRandom.nextInt() call resolves to
java.util.Random.nextInt() and is reported under WG005 instead — the
construction itself (new SecureRandom()) is still reported under WG007 regardless.
As with the other determinism rules, this analysis intentionally stops, without
reporting anything past that point, at two kinds of boundary: a call it cannot resolve to
source it can see, and a call that resolves into a Temporal Activity implementation
(recognized by its @ActivityInterface/@ActivityMethod annotations, or by resolving
only as far as the activity interface's bodyless method) — activity code is not replayed,
so it is exempt from this rule by design.
This means WG007 can produce false negatives (a real violation hidden behind a call it can't see into) but is designed to avoid false positives. If you believe WG007 is flagging or missing something incorrectly, please open an issue with a minimal reproduction.
Found an issue with this rule's detection? Open an issue with a minimal reproduction.