|
63 | 63 | from io import BytesIO, StringIO |
64 | 64 |
|
65 | 65 | logger = logging.getLogger(__name__) |
| 66 | + |
| 67 | +# CVE-2026-31826: refuse to pre-allocate a read buffer for an absurd declared |
| 68 | +# stream /Length. Set to 0 to disable (only safe for fully trusted input). |
| 69 | +MAX_DECLARED_STREAM_LENGTH = 75000000 # 75 MB |
| 70 | + |
| 71 | +# CVE-2026-33123: bound array-based content streams. Set to 0 to disable. |
| 72 | +CONTENT_STREAM_ARRAY_MAX_LENGTH = 10000 # max number of array elements |
| 73 | +MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH = 75000000 # 75 MB total concatenated |
| 74 | + |
66 | 75 | ObjectPrefix = b_("/<[tf(n%") |
67 | 76 | NumberSigns = b_("+-") |
68 | 77 | IndirectPattern = re.compile(b_(r"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]")) |
@@ -835,6 +844,17 @@ def read_from_stream(stream, pdf): |
835 | 844 | t = stream.tell() |
836 | 845 | length = pdf.get_object(length) |
837 | 846 | stream.seek(t, 0) |
| 847 | + # CVE-2026-31826: a crafted /Length (e.g. 2 GB) would make |
| 848 | + # stream.read(length) pre-allocate a huge buffer. Reject it. |
| 849 | + if ( |
| 850 | + isinstance(length, int) |
| 851 | + and MAX_DECLARED_STREAM_LENGTH |
| 852 | + and length > MAX_DECLARED_STREAM_LENGTH |
| 853 | + ): |
| 854 | + raise PdfReadError( |
| 855 | + "Declared stream length (%d bytes) exceeds maximum allowed " |
| 856 | + "(%d bytes)." % (length, MAX_DECLARED_STREAM_LENGTH) |
| 857 | + ) |
838 | 858 | data["__streamdata__"] = stream.read(length) |
839 | 859 | e = readNonWhitespace(stream) |
840 | 860 | ndstream = stream.read(8) |
@@ -895,9 +915,22 @@ def children(self): |
895 | 915 | raise StopIteration |
896 | 916 |
|
897 | 917 | child = self["/First"] |
| 918 | + last = self["/Last"] |
| 919 | + # CVE-2026-27024: a crafted outline whose /Next chain forms a cycle |
| 920 | + # that never reaches /Last made this loop run forever. Track visited |
| 921 | + # nodes and stop on a repeat. |
| 922 | + visited = set() |
898 | 923 | while True: |
| 924 | + child_id = id(child) |
| 925 | + if child_id in visited: |
| 926 | + logger.warning("Cycle detected in TreeObject.children; stopping") |
| 927 | + if sys.version_info >= (3, 5): # PEP 479 |
| 928 | + return |
| 929 | + else: |
| 930 | + raise StopIteration |
| 931 | + visited.add(child_id) |
899 | 932 | yield child |
900 | | - if child == self["/Last"]: |
| 933 | + if child == last: |
901 | 934 | if sys.version_info >= (3, 5): # PEP 479 |
902 | 935 | return |
903 | 936 | else: |
@@ -1191,10 +1224,32 @@ def __init__(self, stream, pdf): |
1191 | 1224 | # multiple StreamObjects to be cat'd together. |
1192 | 1225 | stream = stream.get_object() |
1193 | 1226 | if isinstance(stream, ArrayObject): |
1194 | | - data = b_("") |
| 1227 | + # CVE-2026-33123: bound both the number of array elements and the |
| 1228 | + # total concatenated size so a crafted array-based content stream |
| 1229 | + # cannot exhaust CPU/memory. |
| 1230 | + if ( |
| 1231 | + CONTENT_STREAM_ARRAY_MAX_LENGTH |
| 1232 | + and len(stream) > CONTENT_STREAM_ARRAY_MAX_LENGTH |
| 1233 | + ): |
| 1234 | + raise PdfReadError( |
| 1235 | + "Content stream array has %d elements, exceeding the " |
| 1236 | + "maximum of %d." % (len(stream), CONTENT_STREAM_ARRAY_MAX_LENGTH) |
| 1237 | + ) |
| 1238 | + parts = [] |
| 1239 | + total = 0 |
1195 | 1240 | for s in stream: |
1196 | | - data += b_(s.get_object().get_data()) |
1197 | | - stream = BytesIO(b_(data)) |
| 1241 | + new_data = b_(s.get_object().get_data()) |
| 1242 | + total += len(new_data) |
| 1243 | + if ( |
| 1244 | + MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH |
| 1245 | + and total > MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH |
| 1246 | + ): |
| 1247 | + raise PdfReadError( |
| 1248 | + "Content stream array output exceeds the maximum of " |
| 1249 | + "%d bytes." % MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH |
| 1250 | + ) |
| 1251 | + parts.append(new_data) |
| 1252 | + stream = BytesIO(b_("").join(parts)) |
1198 | 1253 | else: |
1199 | 1254 | stream = BytesIO(b_(stream.get_data())) |
1200 | 1255 | self.__parseContentStream(stream) |
|
0 commit comments