File size: 8,137 Bytes
c8e5eaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""Bounded image container inspection; never decompress pixels or retain image payloads."""
import os
import time

MAX_EDGE = 16_000
MAX_PIXELS = 40_000_000
MAX_FRAMES = 240
MAX_ANIMATION_PIXELS = 400_000_000
MAX_FILE_BYTES = 50 * 1024 * 1024
MAX_STEPS = 500_000
SCAN_SECONDS = 3
JPEG_HEADER_BYTES = 256 * 1024

class ImageBudgetError(ValueError):
    pass

def _bad():
    raise ImageBudgetError('invalid or over-budget image container')

def _budget(width, height, frames=1):
    if not 0 < width <= MAX_EDGE or not 0 < height <= MAX_EDGE or width * height > MAX_PIXELS:
        _bad()
    if not 0 < frames <= MAX_FRAMES or width * height * frames > MAX_ANIMATION_PIXELS:
        _bad()

class _Reader:
    def __init__(self, file):
        self.file = file
        self.size = os.fstat(file.fileno()).st_size
        self.deadline = time.monotonic() + SCAN_SECONDS
        self.steps = 0
        if not 0 < self.size <= MAX_FILE_BYTES: _bad()
    def check(self, length):
        self.steps += 1
        if self.steps > MAX_STEPS or time.monotonic() > self.deadline or length < 0 or self.file.tell() + length > self.size:
            _bad()
    def read(self, length):
        self.check(length)
        if length > 32: _bad()
        result = self.file.read(length)
        if len(result) != length: _bad()
        return result
    def skip(self, length):
        self.check(length)
        self.file.seek(length, 1)
    def pos(self): return self.file.tell()

def _png(r):
    if r.read(8) != b'\x89PNG\r\n\x1a\n': _bad()
    if r.read(8) != b'\x00\x00\x00\rIHDR': _bad()
    header = r.read(13)
    width, height = int.from_bytes(header[:4], 'big'), int.from_bytes(header[4:8], 'big')
    _budget(width, height)
    r.skip(4)
    declared, frames, data, first_control_before_data = None, 0, False, False
    while r.pos() < r.size:
        head = r.read(8); length, kind = int.from_bytes(head[:4], 'big'), head[4:]
        if kind == b'acTL':
            if declared is not None or data or length != 8: _bad()
            declared = int.from_bytes(r.read(8)[:4], 'big'); _budget(width, height, declared)
        elif kind == b'fcTL':
            if declared is None or length != 26: _bad()
            control = r.read(26)
            w, h, x, y = [int.from_bytes(control[n:n+4], 'big') for n in (4,8,12,16)]
            if not w or not h or x + w > width or y + h > height: _bad()
            if frames == 0: first_control_before_data = not data
            frames += 1; _budget(width, height, frames)
        elif kind == b'IDAT':
            data = True; r.skip(length)
        elif kind == b'IEND':
            if length != 0 or not data or (declared is not None and frames != declared): _bad()
            r.skip(4)
            if r.pos() != r.size: _bad()
            total = frames + (0 if first_control_before_data else 1) if declared is not None else 1
            _budget(width, height, total)
            return width, height, total
        else:
            r.skip(length)
        r.skip(4)
    _bad()

def _gif(r):
    if r.read(6) not in (b'GIF87a', b'GIF89a'): _bad()
    head = r.read(7); width, height = int.from_bytes(head[:2],'little'), int.from_bytes(head[2:4],'little')
    _budget(width,height)
    if head[4] & 128: r.skip(3 * (1 << ((head[4] & 7) + 1)))
    def blocks():
        while True:
            size = r.read(1)[0]
            if not size: return
            r.skip(size)
    frames = 0
    while r.pos() < r.size:
        kind = r.read(1)[0]
        if kind == 0x21:
            r.read(1); blocks()
        elif kind == 0x2c:
            image = r.read(9)
            x,y,w,h = [int.from_bytes(image[n:n+2],'little') for n in (0,2,4,6)]
            if not w or not h or x+w > width or y+h > height: _bad()
            frames += 1; _budget(width,height,frames)
            if image[8] & 128: r.skip(3 * (1 << ((image[8] & 7) + 1)))
            if not 2 <= r.read(1)[0] <= 8: _bad()
            blocks()
        elif kind == 0x3b:
            if not frames or r.pos() != r.size: _bad()
            return width,height,frames
        else: _bad()
    _bad()

def _jpeg(r):
    if r.read(2) != b'\xff\xd8': _bad()
    while r.pos() < min(r.size, JPEG_HEADER_BYTES):
        if r.read(1) != b'\xff': _bad()
        marker = r.read(1)[0]
        while marker == 0xff: marker = r.read(1)[0]
        if marker in (0,0xd8,0xd9,0xda) or 0xd0 <= marker <= 0xd7: _bad()
        length = int.from_bytes(r.read(2),'big')
        if length < 2 or r.pos() + length - 2 > min(r.size, JPEG_HEADER_BYTES): _bad()
        if 0xc0 <= marker <= 0xcf and marker not in (0xc4,0xc8,0xcc):
            if length < 8: _bad()
            head = r.read(6); height, width = int.from_bytes(head[1:3],'big'), int.from_bytes(head[3:5],'big')
            _budget(width,height)
            return width,height,1
        r.skip(length-2)
    _bad()

def _webp(r):
    head = r.read(12)
    if head[:4] != b'RIFF' or head[8:] != b'WEBP' or int.from_bytes(head[4:8],'little')+8 != r.size: _bad()
    canvas, frames, animated, animation_header, static = None, 0, False, False, False
    def codec(kind,length):
        if kind == b'VP8 ':
            if length < 10: _bad()
            h = r.read(10)
            if h[3:6] != b'\x9d\x01\x2a' or h[0] & 1: _bad()
            result = int.from_bytes(h[6:8],'little') & 0x3fff, int.from_bytes(h[8:10],'little') & 0x3fff
            r.skip(length-10)
        elif kind == b'VP8L':
            if length < 5: _bad()
            h = r.read(5)
            if h[0] != 0x2f: _bad()
            bits = int.from_bytes(h[1:],'little')
            if bits >> 29: _bad()
            result = (bits & 0x3fff)+1, ((bits >> 14) & 0x3fff)+1
            r.skip(length-5)
        else: _bad()
        _budget(*result)
        return result
    while r.pos() < r.size:
        head = r.read(8); kind, length = head[:4], int.from_bytes(head[4:],'little')
        r.check(length+(length & 1)); end = r.pos()+length
        if kind == b'VP8X':
            if canvas is not None or length != 10: _bad()
            h=r.read(10); canvas=(int.from_bytes(h[4:7],'little')+1,int.from_bytes(h[7:10],'little')+1)
            _budget(*canvas); animated=bool(h[0] & 2)
        elif kind == b'ANIM':
            if not animated or length != 6 or animation_header: _bad()
            animation_header=True; r.skip(length)
        elif kind == b'ANMF':
            if not animated or not animation_header or canvas is None or length < 16: _bad()
            h=r.read(16); x,y=int.from_bytes(h[:3],'little')*2,int.from_bytes(h[3:6],'little')*2
            w,hh=int.from_bytes(h[6:9],'little')+1,int.from_bytes(h[9:12],'little')+1
            if x+w > canvas[0] or y+hh > canvas[1]: _bad()
            frames+=1; _budget(*canvas,frames)
            found=False
            while r.pos()<end:
                chunk=r.read(8); sub,size=chunk[:4],int.from_bytes(chunk[4:],'little')
                if r.pos()+size+(size & 1)>end: _bad()
                if sub in (b'VP8 ',b'VP8L'):
                    if found or codec(sub,size)!=(w,hh): _bad()
                    found=True
                else: r.skip(size)
                if size & 1: r.skip(1)
            if not found: _bad()
        elif kind in (b'VP8 ',b'VP8L'):
            if animated or static: _bad()
            size=codec(kind,length)
            if canvas is not None and size!=canvas: _bad()
            canvas=size;static=True
        else: r.skip(length)
        if r.pos()!=end: _bad()
        if length & 1: r.skip(1)
    if canvas is None or (animated and not frames) or (not animated and not static): _bad()
    return canvas[0], canvas[1], frames if animated else 1

def inspect_image_file(path, ext):
    parser={'png':_png,'gif':_gif,'jpg':_jpeg,'jpeg':_jpeg,'webp':_webp}.get(ext)
    if parser is None: _bad()
    with open(path,'rb') as file:
        width,height,frames=parser(_Reader(file))
    return {'width':width,'height':height,'frames':frames}