001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.utils;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.lang.reflect.*;
022import java.util.*;
023import java.util.concurrent.*;
024
025/**
026 * Bounded, iterative walker that validates already-materialized JSON values against structural safety limits.
027 *
028 * <p>
029 * This helper defends against pathological inputs (excessive nesting, unbounded node counts, cyclic references, and
030 * runaway traversal times) before a value is serialized or otherwise consumed.  It walks only in-memory maps,
031 * collections, arrays, and scalar leaves; it never resolves <js>"$ref"</js> or performs any I/O.
032 *
033 * <p>
034 * All violations are reported as neutral {@link IllegalArgumentException}s.  Typed or adaptor callers are responsible
035 * for mapping these to the appropriate JSON-RPC error code, which keeps this core class free of any revision-specific
036 * error policy.
037 */
038public final class JsonValueSafety {
039
040   /** Maximum allowed nesting depth of the walked value. */
041   public static final int MAX_DEPTH = 64;
042
043   /** Maximum allowed number of distinct nodes (containers and leaves) in the walked value. */
044   public static final int MAX_NODES = 10_000;
045
046   /** Maximum allowed wall-clock time, in milliseconds, for a single traversal. */
047   public static final long MAX_TRAVERSAL_MILLIS = 100;
048
049   private JsonValueSafety() {}
050
051   /**
052    * Computes a fresh traversal deadline from the current time.
053    *
054    * @return An absolute deadline in {@link System#nanoTime()} units, {@link #MAX_TRAVERSAL_MILLIS} milliseconds in the future.
055    */
056   public static long deadlineNanos() {
057      return System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(MAX_TRAVERSAL_MILLIS);
058   }
059
060   /**
061    * Validates the specified value against the structural safety limits using a fresh {@link #deadlineNanos() deadline}.
062    *
063    * @param value The value to walk.  Can be <jk>null</jk>, in which case this method returns immediately.
064    * @param label A human-readable label describing the value; used as a prefix in violation messages.
065    * @throws IllegalArgumentException If the value violates any of the depth, node-count, traversal-time, or JSON-shape constraints.
066    */
067   public static void check(Object value, String label) {
068      check(value, label, deadlineNanos());
069   }
070
071   /**
072    * Validates the specified value against the structural safety limits using the specified traversal deadline.
073    *
074    * @param root The value to walk.  Can be <jk>null</jk>, in which case this method returns immediately.
075    * @param label A human-readable label describing the value; used as a prefix in violation messages.
076    * @param deadlineNanos An absolute deadline in {@link System#nanoTime()} units after which the traversal is aborted.
077    * @throws IllegalArgumentException
078    *    If the value exceeds {@link #MAX_DEPTH}, exceeds {@link #MAX_NODES}, exceeds the traversal deadline, contains a
079    *    non-string JSON object key, or contains a leaf that is not a JSON value type ({@link String}, {@link Number}, or
080    *    {@link Boolean}).
081    */
082   public static void check(Object root, String label, long deadlineNanos) {
083      if (root == null)
084         return;
085      var seen = Collections.newSetFromMap(new IdentityHashMap<Object,Boolean>());
086      var stack = new ArrayDeque<Framed>();
087      stack.push(new Framed(root, 1));
088      var nodes = 0;
089      while (! stack.isEmpty()) {
090         // Bounded per-iteration work runs before the deadline check so a structurally-oversized input
091         // deterministically reports its depth/node-count violation instead of racing the wall clock.
092         var frame = stack.pop();
093         if (frame.depth() > MAX_DEPTH)
094            throw iaex("%s exceeds maximum nesting depth of %s", label, MAX_DEPTH);
095         nodes = visitFrame(frame, label, seen, stack, nodes);
096         if (System.nanoTime() >= deadlineNanos)
097            throw iaex("%s traversal exceeded %s ms", label, MAX_TRAVERSAL_MILLIS);
098      }
099   }
100
101   /**
102    * Processes one popped stack frame: counts it toward the node budget, and - if it's an unseen container -
103    * pushes its children onto {@code stack} for further traversal.
104    *
105    * @return The updated node count.
106    */
107   private static int visitFrame(Framed frame, String label, Set<Object> seen, ArrayDeque<Framed> stack, int nodes) {
108      var value = frame.value();
109      if (value == null)
110         return checkNodeBudget(nodes, label);
111      var container = value instanceof Map<?,?> || value instanceof Collection<?> || value.getClass().isArray();
112      if (container && ! seen.add(value))
113         return nodes;
114      nodes = checkNodeBudget(nodes, label);
115      pushChildren(value, frame.depth(), label, stack);
116      return nodes;
117   }
118
119   private static int checkNodeBudget(int nodes, String label) {
120      if (++nodes > MAX_NODES)
121         throw iaex("%s exceeds maximum node count of %s", label, MAX_NODES);
122      return nodes;
123   }
124
125   private static void pushChildren(Object value, int depth, String label, ArrayDeque<Framed> stack) {
126      if (value instanceof Map<?,?> value2) {
127         for (var entry : value2.entrySet()) {
128            if (! (entry.getKey() instanceof String))
129               throw iaex("%s contains non-string JSON object key %s", label, entry.getKey());
130            stack.push(new Framed(entry.getValue(), depth + 1));
131         }
132      } else if (value instanceof Collection<?> value2) {
133         for (var child : value2)
134            stack.push(new Framed(child, depth + 1));
135      } else if (value.getClass().isArray()) {
136         for (var i = 0; i < Array.getLength(value); i++)
137            stack.push(new Framed(Array.get(value, i), depth + 1));
138      } else if (! (value instanceof String || value instanceof Number || value instanceof Boolean))
139         throw iaex("%s contains non-JSON value type %s", label, value.getClass().getName());
140   }
141
142   /**
143    * Computes the number of nanoseconds remaining before the specified deadline.
144    *
145    * @param deadlineNanos An absolute deadline in {@link System#nanoTime()} units.
146    * @return The remaining nanoseconds, or <c>0</c> if the deadline has already passed.
147    */
148   public static long remainingNanos(long deadlineNanos) {
149      return Math.max(0, deadlineNanos - System.nanoTime());
150   }
151
152   private record Framed(Object value, int depth) {}
153}