Coverage for src/backend/InvenTree/InvenTree/sanitizer.py: 45%

16 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""Functions to sanitize user input files.""" 

2 

3import nh3 

4 

5# Allowed CSS properties for SVG sanitization (combines general CSS and SVG-specific properties) 

6_SVG_ALLOWED_CSS_PROPERTIES = frozenset([ 

7 # General CSS (matching bleach's original ALLOWED_CSS_PROPERTIES) 

8 'azimuth', 

9 'background-color', 

10 'border-bottom-color', 

11 'border-collapse', 

12 'border-color', 

13 'border-left-color', 

14 'border-right-color', 

15 'border-top-color', 

16 'clear', 

17 'color', 

18 'cursor', 

19 'direction', 

20 'display', 

21 'elevation', 

22 'float', 

23 'font', 

24 'font-family', 

25 'font-size', 

26 'font-style', 

27 'font-variant', 

28 'font-weight', 

29 'height', 

30 'letter-spacing', 

31 'line-height', 

32 'overflow', 

33 'pause', 

34 'pause-after', 

35 'pause-before', 

36 'pitch', 

37 'pitch-range', 

38 'richness', 

39 'speak', 

40 'speak-header', 

41 'speak-numeral', 

42 'speak-punctuation', 

43 'speech-rate', 

44 'stress', 

45 'text-align', 

46 'text-decoration', 

47 'text-indent', 

48 'unicode-bidi', 

49 'vertical-align', 

50 'voice-family', 

51 'volume', 

52 'white-space', 

53 'width', 

54 # SVG-specific CSS (matching bleach's ALLOWED_SVG_PROPERTIES) 

55 'fill', 

56 'fill-opacity', 

57 'fill-rule', 

58 'stroke', 

59 'stroke-linecap', 

60 'stroke-linejoin', 

61 'stroke-opacity', 

62 'stroke-width', 

63]) 

64 

65ALLOWED_ELEMENTS_SVG = [ 

66 'a', 

67 'animate', 

68 'animateColor', 

69 'animateMotion', 

70 'animateTransform', 

71 'circle', 

72 'defs', 

73 'desc', 

74 'ellipse', 

75 'font-face', 

76 'font-face-name', 

77 'font-face-src', 

78 'g', 

79 'glyph', 

80 'hkern', 

81 'linearGradient', 

82 'line', 

83 'marker', 

84 'metadata', 

85 'missing-glyph', 

86 'mpath', 

87 'path', 

88 'polygon', 

89 'polyline', 

90 'radialGradient', 

91 'rect', 

92 'set', 

93 'stop', 

94 'svg', 

95 'switch', 

96 'text', 

97 'title', 

98 'tspan', 

99 'use', 

100] 

101 

102ALLOWED_ATTRIBUTES_SVG = [ 

103 'accent-height', 

104 'accumulate', 

105 'additive', 

106 'alphabetic', 

107 'arabic-form', 

108 'ascent', 

109 'attributeName', 

110 'attributeType', 

111 'baseProfile', 

112 'bbox', 

113 'begin', 

114 'by', 

115 'calcMode', 

116 'cap-height', 

117 'class', 

118 'color', 

119 'color-rendering', 

120 'content', 

121 'cx', 

122 'cy', 

123 'd', 

124 'dx', 

125 'dy', 

126 'descent', 

127 'display', 

128 'dur', 

129 'end', 

130 'fill', 

131 'fill-opacity', 

132 'fill-rule', 

133 'font-family', 

134 'font-size', 

135 'font-stretch', 

136 'font-style', 

137 'font-variant', 

138 'font-weight', 

139 'from', 

140 'fx', 

141 'fy', 

142 'g1', 

143 'g2', 

144 'glyph-name', 

145 'gradientUnits', 

146 'hanging', 

147 'height', 

148 'horiz-adv-x', 

149 'horiz-origin-x', 

150 'id', 

151 'ideographic', 

152 'k', 

153 'keyPoints', 

154 'keySplines', 

155 'keyTimes', 

156 'lang', 

157 'marker-end', 

158 'marker-mid', 

159 'marker-start', 

160 'markerHeight', 

161 'markerUnits', 

162 'markerWidth', 

163 'mathematical', 

164 'max', 

165 'min', 

166 'name', 

167 'offset', 

168 'opacity', 

169 'orient', 

170 'origin', 

171 'overline-position', 

172 'overline-thickness', 

173 'panose-1', 

174 'path', 

175 'pathLength', 

176 'points', 

177 'preserveAspectRatio', 

178 'r', 

179 'refX', 

180 'refY', 

181 'repeatCount', 

182 'repeatDur', 

183 'requiredExtensions', 

184 'requiredFeatures', 

185 'restart', 

186 'rotate', 

187 'rx', 

188 'ry', 

189 'slope', 

190 'stemh', 

191 'stemv', 

192 'stop-color', 

193 'stop-opacity', 

194 'strikethrough-position', 

195 'strikethrough-thickness', 

196 'stroke', 

197 'stroke-dasharray', 

198 'stroke-dashoffset', 

199 'stroke-linecap', 

200 'stroke-linejoin', 

201 'stroke-miterlimit', 

202 'stroke-opacity', 

203 'stroke-width', 

204 'systemLanguage', 

205 'target', 

206 'text-anchor', 

207 'to', 

208 'transform', 

209 'type', 

210 'u1', 

211 'u2', 

212 'underline-position', 

213 'underline-thickness', 

214 'unicode', 

215 'unicode-range', 

216 'units-per-em', 

217 'values', 

218 'version', 

219 'viewBox', 

220 'visibility', 

221 'width', 

222 'widths', 

223 'x', 

224 'x-height', 

225 'x1', 

226 'x2', 

227 'xlink:actuate', 

228 'xlink:arcrole', 

229 'xlink:href', 

230 'xlink:role', 

231 'xlink:show', 

232 'xlink:title', 

233 'xlink:type', 

234 'xml:base', 

235 'xml:lang', 

236 'xml:space', 

237 'xmlns', 

238 'xmlns:xlink', 

239 'y', 

240 'y1', 

241 'y2', 

242 'zoomAndPan', 

243 'style', 

244] 

245 

246# Default allowlists (matching bleach's original defaults) 

247# TODO: I do not see us needing a bunch of these but I do not want to introduce a breaking change; we might want to narroy this down with the next breaking change 

248DEFAULT_TAGS = frozenset([ 

249 'a', 

250 'abbr', 

251 'acronym', 

252 'b', 

253 'blockquote', 

254 'code', 

255 'em', 

256 'i', 

257 'li', 

258 'ol', 

259 'strong', 

260 'ul', 

261]) 

262DEAFAULT_ATTRS = {'a': {'href', 'title'}, 'abbr': {'title'}, 'acronym': {'title'}} 

263DEFAULT_CSS = frozenset([ 

264 'azimuth', 

265 'background-color', 

266 'border-bottom-color', 

267 'border-collapse', 

268 'border-color', 

269 'border-left-color', 

270 'border-right-color', 

271 'border-top-color', 

272 'clear', 

273 'color', 

274 'cursor', 

275 'direction', 

276 'display', 

277 'elevation', 

278 'float', 

279 'font', 

280 'font-family', 

281 'font-size', 

282 'font-style', 

283 'font-variant', 

284 'font-weight', 

285 'height', 

286 'letter-spacing', 

287 'line-height', 

288 'overflow', 

289 'pause', 

290 'pause-after', 

291 'pause-before', 

292 'pitch', 

293 'pitch-range', 

294 'richness', 

295 'speak', 

296 'speak-header', 

297 'speak-numeral', 

298 'speak-punctuation', 

299 'speech-rate', 

300 'stress', 

301 'text-align', 

302 'text-decoration', 

303 'text-indent', 

304 'unicode-bidi', 

305 'vertical-align', 

306 'voice-family', 

307 'volume', 

308 'white-space', 

309 'width', 

310]) 

311# TODO: We might want to respect the setting EXTRA_URL_SCHEMES here but that would be breaking 

312DEFAULT_PROTOCOLS = frozenset(['http', 'https', 'mailto']) 

313 

314 

315def sanitize_svg( 

316 file_data, 

317 strip: bool = True, 

318 elements: list[str] = ALLOWED_ELEMENTS_SVG, 

319 attributes: list[str] = ALLOWED_ATTRIBUTES_SVG, 

320) -> str: 

321 """Sanitize a SVG file. 

322 

323 Args: 

324 file_data (str): SVG as string. 

325 strip (bool, optional): Should invalid elements get removed. Defaults to True. 

326 elements (str, optional): Allowed elements. Defaults to ALLOWED_ELEMENTS_SVG. 

327 attributes (str, optional): Allowed attributes. Defaults to ALLOWED_ATTRIBUTES_SVG. 

328 

329 Returns: 

330 str: Sanitzied SVG file. 

331 """ 

332 # Handle byte-encoded data 

333 if isinstance(file_data, bytes): 

334 file_data = file_data.decode('utf-8') 

335 

336 # nh3 requires attributes as dict[str, set[str]]; convert from list (allowed for all elements) 

337 attrs_dict = {elem: set(attributes) for elem in elements} 

338 

339 cleaned = nh3.clean( 

340 file_data, 

341 tags=set(elements), 

342 attributes=attrs_dict, 

343 filter_style_properties=_SVG_ALLOWED_CSS_PROPERTIES, 

344 strip_comments=strip, 

345 link_rel=None, 

346 ) 

347 

348 # Replace non-breaking spaces with regular spaces to prevent SVG rendering issues 

349 for nbsp in [' ', ' ']: 

350 cleaned = cleaned.replace(nbsp, ' ') 

351 

352 return cleaned