Coverage for agentos/tools/csp.py: 0%

132 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-03 08:08 +0800

1""" 

2CSP — Content Security Policy builder and validator. 

3 

4Supports: 

5 - Fluent API for building CSP headers 

6 - Source lists (self, none, urls, hashes, nonces, strict-dynamic) 

7 - Directives: default-src, script-src, style-src, img-src, connect-src, font-src, 

8 object-src, media-src, frame-src, frame-ancestors, form-action, base-uri, 

9 report-uri, report-to, upgrade-insecure-requests, block-all-mixed-content, 

10 sandbox, worker-src, manifest-src, prefetch-src, navigate-to 

11 - Nonce generation 

12 - Validation of directives 

13 - Serialize to header string 

14""" 

15 

16from __future__ import annotations 

17 

18import secrets 

19from typing import List, Optional, Set, Union 

20 

21 

22# ============================================================================ 

23# Constants 

24# ============================================================================ 

25 

26ALL_DIRECTIVES = frozenset({ 

27 "default-src", "script-src", "script-src-elem", "script-src-attr", 

28 "style-src", "style-src-elem", "style-src-attr", 

29 "img-src", "connect-src", "font-src", "object-src", "media-src", 

30 "frame-src", "frame-ancestors", "form-action", "base-uri", 

31 "report-uri", "report-to", "sandbox", 

32 "worker-src", "manifest-src", "prefetch-src", "navigate-to", 

33 "child-src", "fenced-frame-src", 

34 "upgrade-insecure-requests", "block-all-mixed-content", 

35 "require-trusted-types-for", 

36}) 

37 

38FLAG_DIRECTIVES = frozenset({"upgrade-insecure-requests", "block-all-mixed-content"}) 

39 

40KEYWORD_SOURCES = frozenset({ 

41 "'self'", "'none'", "'strict-dynamic'", 

42 "'unsafe-inline'", "'unsafe-eval'", 

43 "'unsafe-hashes'", "'unsafe-allow-redirects'", 

44 "'wasm-unsafe-eval'", 

45}) 

46 

47 

48# ============================================================================ 

49# Nonce 

50# ============================================================================ 

51 

52def generate_nonce(length: int = 32) -> str: 

53 """Generate a cryptographically random base64 nonce.""" 

54 return secrets.token_urlsafe(length) 

55 

56 

57# ============================================================================ 

58# CSP 

59# ============================================================================ 

60 

61class CSP: 

62 """Fluent Content Security Policy builder. 

63 

64 Usage: 

65 csp = (CSP() 

66 .default_src("'self'") 

67 .script_src("'self'", "'strict-dynamic'", nonce=generate_nonce()) 

68 .style_src("'self'", "https://fonts.googleapis.com") 

69 .img_src("*") 

70 .upgrade_insecure_requests() 

71 ) 

72 

73 header = csp.to_header() 

74 # default-src 'self'; script-src 'self' 'strict-dynamic' 'nonce-abc123'; ... 

75 """ 

76 

77 def __init__(self): 

78 self._directives: dict = {} 

79 

80 # ---------- Directive setters ---------- 

81 

82 def default_src(self, *sources: str) -> CSP: 

83 return self._set("default-src", *sources) 

84 

85 def script_src(self, *sources: str, nonce: Optional[str] = None, hashes: Optional[List[str]] = None) -> CSP: 

86 if nonce: 

87 sources = sources + (f"'nonce-{nonce}'",) 

88 if hashes: 

89 sources = sources + tuple(h for h in hashes) 

90 return self._set("script-src", *sources) 

91 

92 def script_src_elem(self, *sources: str, nonce: Optional[str] = None) -> CSP: 

93 if nonce: 

94 sources = sources + (f"'nonce-{nonce}'",) 

95 return self._set("script-src-elem", *sources) 

96 

97 def script_src_attr(self, *sources: str) -> CSP: 

98 return self._set("script-src-attr", *sources) 

99 

100 def style_src(self, *sources: str, nonce: Optional[str] = None) -> CSP: 

101 if nonce: 

102 sources = sources + (f"'nonce-{nonce}'",) 

103 return self._set("style-src", *sources) 

104 

105 def style_src_elem(self, *sources: str, nonce: Optional[str] = None) -> CSP: 

106 if nonce: 

107 sources = sources + (f"'nonce-{nonce}'",) 

108 return self._set("style-src-elem", *sources) 

109 

110 def style_src_attr(self, *sources: str) -> CSP: 

111 return self._set("style-src-attr", *sources) 

112 

113 def img_src(self, *sources: str) -> CSP: 

114 return self._set("img-src", *sources) 

115 

116 def connect_src(self, *sources: str) -> CSP: 

117 return self._set("connect-src", *sources) 

118 

119 def font_src(self, *sources: str) -> CSP: 

120 return self._set("font-src", *sources) 

121 

122 def object_src(self, *sources: str) -> CSP: 

123 return self._set("object-src", *sources) 

124 

125 def media_src(self, *sources: str) -> CSP: 

126 return self._set("media-src", *sources) 

127 

128 def frame_src(self, *sources: str) -> CSP: 

129 return self._set("frame-src", *sources) 

130 

131 def frame_ancestors(self, *sources: str) -> CSP: 

132 return self._set("frame-ancestors", *sources) 

133 

134 def form_action(self, *sources: str) -> CSP: 

135 return self._set("form-action", *sources) 

136 

137 def base_uri(self, *sources: str) -> CSP: 

138 return self._set("base-uri", *sources) 

139 

140 def worker_src(self, *sources: str) -> CSP: 

141 return self._set("worker-src", *sources) 

142 

143 def child_src(self, *sources: str) -> CSP: 

144 return self._set("child-src", *sources) 

145 

146 def manifest_src(self, *sources: str) -> CSP: 

147 return self._set("manifest-src", *sources) 

148 

149 def prefetch_src(self, *sources: str) -> CSP: 

150 return self._set("prefetch-src", *sources) 

151 

152 def navigate_to(self, *sources: str) -> CSP: 

153 return self._set("navigate-to", *sources) 

154 

155 def sandbox(self, *flags: str) -> CSP: 

156 value = " ".join(flags) if flags else "" 

157 self._directives["sandbox"] = value 

158 return self 

159 

160 def report_uri(self, *uris: str) -> CSP: 

161 return self._set("report-uri", *uris) 

162 

163 def report_to(self, group: str) -> CSP: 

164 self._directives["report-to"] = group 

165 return self 

166 

167 # ---------- Flags ---------- 

168 

169 def upgrade_insecure_requests(self) -> CSP: 

170 self._directives["upgrade-insecure-requests"] = "" 

171 return self 

172 

173 def block_all_mixed_content(self) -> CSP: 

174 self._directives["block-all-mixed-content"] = "" 

175 return self 

176 

177 # ---------- Utility ---------- 

178 

179 def _set(self, directive: str, *sources: str) -> CSP: 

180 existing = self._directives.get(directive) 

181 if isinstance(existing, list): 

182 existing.extend(sources) 

183 else: 

184 self._directives[directive] = list(sources) 

185 return self 

186 

187 def to_header(self) -> str: 

188 """Serialize to CSP header string.""" 

189 parts = [] 

190 for directive, value in self._directives.items(): 

191 if value == "": 

192 parts.append(directive) 

193 elif isinstance(value, list): 

194 sources_str = " ".join(value) 

195 parts.append(f"{directive} {sources_str}") 

196 else: 

197 parts.append(f"{directive} {value}") 

198 return "; ".join(parts) 

199 

200 def to_dict(self) -> dict: 

201 return dict(self._directives) 

202 

203 @classmethod 

204 def parse(cls, header: str) -> CSP: 

205 """Parse a CSP header string into a CSP builder.""" 

206 csp = cls() 

207 for part in header.split(";"): 

208 part = part.strip() 

209 if not part: 

210 continue 

211 tokens = part.split() 

212 directive = tokens[0] 

213 if directive in FLAG_DIRECTIVES: 

214 csp._directives[directive] = "" 

215 elif directive == "sandbox": 

216 csp._directives[directive] = " ".join(tokens[1:]) if len(tokens) > 1 else "" 

217 elif directive == "report-to": 

218 csp._directives[directive] = tokens[1] if len(tokens) > 1 else "" 

219 else: 

220 csp._directives[directive] = tokens[1:] 

221 return csp 

222 

223 @classmethod 

224 def is_valid_directive(cls, name: str) -> bool: 

225 return name in ALL_DIRECTIVES 

226 

227 @classmethod 

228 def strict_policy(cls, nonce: Optional[str] = None) -> CSP: 

229 """Pre-built strict CSP with nonce or hash-based approach.""" 

230 csp = cls() 

231 csp.base_uri("'self'") 

232 csp.default_src("'self'") 

233 csp.object_src("'none'") 

234 if nonce: 

235 csp.script_src("'strict-dynamic'", nonce=nonce) 

236 csp.style_src(nonce=nonce) 

237 else: 

238 csp.script_src("'self'") 

239 csp.style_src("'self'") 

240 csp.img_src("*", "data:") 

241 csp.font_src("'self'", "data:") 

242 csp.connect_src("'self'") 

243 csp.frame_ancestors("'none'") 

244 return csp