Skip to content

Commit a366a09

Browse files
authored
feat: chromium recon extensions (dotsec ext) + browser polish (#10)
* feat(ext): ext list + manifest parsing * feat(ext): github provider with sha256 verification * feat(ext): webstore .crx provider * feat(ext): ext sync orchestration + idempotence * feat(ext): CyberChef managed-bookmark policy * feat(browser): mount extensions + bookmarks policy * fix(ext): isolate each fetch in a subshell; correct crx zip offset + prodversion * feat(ext): pin curated 8-extension manifest * fix(browser): load all extensions in one comma-separated --load-extension * feat(ext): add Dark Reader * feat(browser): pin extensions, show bookmark bar, grant host access Replace managed-bookmarks.json with managed-policies.json (ManagedBookmarks + BookmarkBarEnabled). The entrypoint pre-seeds the Chromium profile so each unpacked extension is pinned to the toolbar and gets its declared host access on all sites; toolbar_pin/runtime_allowed_hosts policies do not apply to unpacked extensions. Extension IDs are derived from SHA-256 of their path.
1 parent 06a1bca commit a366a09

8 files changed

Lines changed: 393 additions & 6 deletions

File tree

bin/dotsec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ if [[ -z "${WORKSPACE_ROOT:-}" ]]; then
2121
fi
2222

2323
DOTSEC_LIB="${DOTSEC_HOME}/lib"
24-
for _m in ui core secrets dashboard proxy exegol engagement status listener; do
24+
for _m in ui core secrets dashboard proxy exegol engagement status listener ext; do
2525
# shellcheck source=/dev/null
2626
source "${DOTSEC_LIB}/${_m}.sh"
2727
done
@@ -82,6 +82,7 @@ case "${1:-help}" in
8282
env) shift; cmd_env "$@";;
8383
status) shift; cmd_status "$@";;
8484
listener) shift; cmd_listener "$@";;
85+
ext) shift; cmd_ext "$@";;
8586
info) cmd_info;;
8687
completions) shift; exec "${DOTSEC_HOME}/bin/dotsec-completions" "$@";;
8788
help|--help|-h) usage;;

chromium/entrypoint.sh

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,35 @@ if [ -n "$HTTP_PROXY" ] && [ -z "$CA_FILE" ]; then
5050
echo "[!] No CA cert mounted: ignoring cert errors"
5151
fi
5252

53-
# Preload extensions if provided
53+
# Preload extensions if provided. Chromium keeps only ONE --load-extension, so
54+
# every unpacked dir must be passed as a single comma-separated value.
5455
if [ -d /extensions ] && [ "$(ls -A /extensions 2>/dev/null)" ]; then
55-
EXT_FLAGS=""
56+
EXT_LIST=""
5657
for ext in /extensions/*/; do
57-
EXT_FLAGS="$EXT_FLAGS --load-extension=$ext"
58+
[ -f "${ext}manifest.json" ] || continue
59+
EXT_LIST="${EXT_LIST:+${EXT_LIST},}${ext%/}"
5860
done
59-
CHROMIUM_FLAGS="$CHROMIUM_FLAGS $EXT_FLAGS"
60-
echo "[+] Extensions loaded"
61+
if [ -n "$EXT_LIST" ]; then
62+
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --load-extension=$EXT_LIST"
63+
# Pin each extension to the toolbar. The toolbar_pin POLICY does not
64+
# apply to unpacked extensions, so pre-seed the profile instead. An
65+
# unpacked ID = first 16 bytes of SHA-256(abs path), 0-9a-f -> a-p.
66+
PROFILE="${HOME:-/root}/.config/chromium/Default"
67+
PINS=""; SETTINGS=""
68+
for ext in /extensions/*/; do
69+
[ -f "${ext}manifest.json" ] || continue
70+
eid=$(printf '%s' "${ext%/}" | sha256sum | cut -c1-32 | tr '0-9a-f' 'a-p')
71+
PINS="${PINS:+$PINS,}\"$eid\""
72+
# withholding_permissions=false → grant declared host access on all sites
73+
SETTINGS="${SETTINGS:+$SETTINGS,}\"$eid\":{\"withholding_permissions\":false}"
74+
done
75+
if [ -n "$PINS" ] && [ ! -f "$PROFILE/Preferences" ]; then
76+
mkdir -p "$PROFILE"
77+
printf '{"extensions":{"pinned_extensions":[%s],"toolbar":[%s],"settings":{%s}}}\n' \
78+
"$PINS" "$PINS" "$SETTINGS" > "$PROFILE/Preferences"
79+
fi
80+
echo "[+] Extensions loaded + pinned: $EXT_LIST"
81+
fi
6182
fi
6283

6384
echo "[*] Launching Chromium..."

chromium/extensions.list

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# dotsec browser extensions — recon/bug-bounty, pinned & fetched by `dotsec ext sync`.
2+
# Format: name | provider | source | ref | sha256 | subdir
3+
# github : source=owner/repo ref=git tag sha256=tarball subdir=path of manifest.json ('.' = root)
4+
# webstore : source=extension id ref=ext version (info/marker) sha256=.crx (subdir unused)
5+
# A sha256 mismatch on sync flags an upstream change → review and re-pin.
6+
7+
js-recon-buddy | github | TheArqsz/JSRecon-Buddy | v1.20.2 | 7efd1caad51e7c53b2e051c56056a153c22ca32d006fdebc6fef0e27f925b826 | .
8+
keyfinder | github | momenbasel/KeyFinder | v2.1.1 | 6b97ec115aea681d56f882d60637dfad2222379d82123d61d93b1b279f61ef81 | .
9+
dotgit | github | davtur19/DotGit | 5.2 | f9254d5ff7b63697ca6856bcd905d92488b53a158a7e8043a30186425341851d | .
10+
jwt-inspector | github | bugjam/jwt-inspector | v1.8 | 3cf2ed77de8263b23dd44da9386a145526a004175134795026334ac46b160c98 | .
11+
findsomething | webstore | kfhniponecokdefffkpagipffdefeldb | 2.1.12 | 87a787ce272452b4139f9c5b8a823181fd38c1d59735137e9ca4de0d4744cd15 |
12+
wappalyzer | webstore | gppongmhjkpfnbhagpmjfkannfbllamg | 6.12.2 | 5bf332e9ac547e7b62fc35493cfa9aa51a924dc73644f5dbfa63878057c7653d |
13+
hacktools | webstore | cmbndhnoonmghfofefkcccljbkdpamhi | 0.5.0 | 60accc0ac753eb12183e1db34dddeaa66d2874f8a5ffa918f49b7fc06586560a |
14+
cookie-editor | webstore | hlkenndednhfkekhgcdicdfddnkalmdm | 1.13.0 | 754a072a5b6327d27a0d4d6e299a8e28b2a5fafbd595ccb6bfcd4090ae8eb038 |
15+
darkreader | webstore | eimadpbcbfnmbkopoojfekhnkhdbieeh | 4.9.125 | 8c086983256e70789fe9f2765542683ad3c0707521ec174010cafd2697687016 |

chromium/managed-policies.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ManagedBookmarks": [
3+
{ "toplevel_name": "dotsec" },
4+
{ "name": "CyberChef", "url": "https://gchq.github.io/CyberChef/" }
5+
],
6+
"BookmarkBarEnabled": true
7+
}

lib/ext.sh

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env bash
2+
# ─── lib/ext.sh ─── browser extension manager (sync/list) ──
3+
4+
__ext_manifest() {
5+
echo "${DOTSEC_EXT_MANIFEST:-${DOTSEC_HOME}/chromium/extensions.list}"
6+
}
7+
8+
__ext_dir() {
9+
echo "${DOTSEC_EXT_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/dotenvsec/extensions}"
10+
}
11+
12+
# Trim leading/trailing whitespace (pure bash, no xargs surprises on quotes).
13+
__ext_trim() {
14+
local s="$1"
15+
s="${s#"${s%%[![:space:]]*}"}"
16+
s="${s%"${s##*[![:space:]]}"}"
17+
printf '%s' "$s"
18+
}
19+
20+
# Parse the manifest, invoking <callback> with the 6 trimmed fields per entry.
21+
# Manifest line: name | provider | source | ref | sha256 | subdir (# = comment)
22+
__ext_each() {
23+
local cb="$1" manifest name provider src ref sha subdir
24+
manifest="$(__ext_manifest)"
25+
if [[ ! -f "$manifest" ]]; then
26+
printf '%b\n' "${RED}[!] No manifest: ${manifest}${RESET}" >&2
27+
return 1
28+
fi
29+
while IFS='|' read -r name provider src ref sha subdir; do
30+
name="$(__ext_trim "${name:-}")"
31+
[[ -z "$name" || "$name" == \#* ]] && continue
32+
provider="$(__ext_trim "${provider:-}")"
33+
src="$(__ext_trim "${src:-}")"
34+
ref="$(__ext_trim "${ref:-}")"
35+
sha="$(__ext_trim "${sha:-}")"
36+
subdir="$(__ext_trim "${subdir:-}")"
37+
"$cb" "$name" "$provider" "$src" "$ref" "$sha" "$subdir"
38+
done < "$manifest"
39+
}
40+
41+
__ext_list_one() {
42+
local name="$1" provider="$2" ref="$4"
43+
local state="${RED}missing${RESET}"
44+
[[ -f "$(__ext_dir)/${name}/manifest.json" ]] && state="${GREEN}ok${RESET}"
45+
printf ' %-18s %-9s %-12s %b\n' "$name" "$provider" "$ref" "$state"
46+
}
47+
48+
ext_list() {
49+
printf '%b\n' "${DIM}Extensions dir: $(__ext_dir)${RESET}"
50+
__ext_each __ext_list_one
51+
}
52+
53+
# __ext_fetch_github <name> <repo> <tag> <sha256> <subdir>
54+
# Downloads the tag tarball, verifies sha256, extracts the unpacked extension
55+
# (subdir, or repo root) into $DOTSEC_EXT_DIR/<name>/. The body runs in a
56+
# subshell so the tmp-cleanup trap cannot fire on a caller's later return.
57+
__ext_fetch_github() {
58+
local name="$1" repo="$2" tag="$3" want="$4" subdir="${5:-.}"
59+
local dir; dir="$(__ext_dir)"
60+
local url="https://github.com/${repo}/archive/refs/tags/${tag}.tar.gz"
61+
(
62+
set -euo pipefail
63+
local tmp; tmp="$(mktemp -d)"
64+
trap 'rm -rf "$tmp"' EXIT
65+
if ! curl -fsSL "$url" -o "$tmp/dl.tar.gz"; then
66+
printf '%b\n' " ${RED}[!] download failed: ${url}${RESET}" >&2
67+
exit 1
68+
fi
69+
local got; got="$(sha256sum "$tmp/dl.tar.gz" | cut -d' ' -f1)"
70+
if [[ -n "$want" && "$got" != "$want" ]]; then
71+
printf '%b\n' " ${RED}[!] sha256 mismatch for ${name}${RESET}" >&2
72+
printf '%b\n' " want ${want}" >&2
73+
printf '%b\n' " got ${got}" >&2
74+
exit 1
75+
fi
76+
mkdir -p "$tmp/x"
77+
tar -xzf "$tmp/dl.tar.gz" -C "$tmp/x" --strip-components=1
78+
local src="$tmp/x"
79+
if [[ "$subdir" != "." && -n "$subdir" ]]; then
80+
src="$tmp/x/$subdir"
81+
fi
82+
if [[ ! -f "$src/manifest.json" ]]; then
83+
printf '%b\n' " ${RED}[!] no manifest.json in ${name} (${subdir})${RESET}" >&2
84+
exit 1
85+
fi
86+
rm -rf "${dir:?}/$name"
87+
mkdir -p "$dir/$name"
88+
cp -a "$src/." "$dir/$name/"
89+
echo "$tag" > "$dir/$name/.dotsec-version"
90+
printf '%b\n' " ${GREEN}[+]${RESET} ${name} ${DIM}(${tag})${RESET}"
91+
)
92+
}
93+
94+
# __ext_fetch_webstore <name> <id> <version> <sha256>
95+
# Downloads the pinned .crx, verifies sha256, carves out the zip payload
96+
# (CRX2/CRX3 header skipped via the first PK\x03\x04), unzips into
97+
# $DOTSEC_EXT_DIR/<name>/.
98+
__ext_fetch_webstore() {
99+
local name="$1" id="$2" ver="$3" want="$4"
100+
local dir; dir="$(__ext_dir)"
101+
# prodversion is the BROWSER version Google serves a compatible crx for, not
102+
# the extension version (the URL always returns the latest; ver pins for the
103+
# idempotence marker and a sha256 mismatch flags any upstream change).
104+
local browser_ver="${DOTSEC_CHROME_MAJOR:-149}"
105+
local url="https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&prodversion=${browser_ver}&x=id%3D${id}%26installsource%3Dondemand%26uc"
106+
(
107+
set -euo pipefail
108+
local tmp; tmp="$(mktemp -d)"
109+
trap 'rm -rf "$tmp"' EXIT
110+
if ! curl -fsSL "$url" -o "$tmp/e.crx"; then
111+
printf '%b\n' " ${RED}[!] crx download failed: ${name}${RESET}" >&2
112+
exit 1
113+
fi
114+
local got; got="$(sha256sum "$tmp/e.crx" | cut -d' ' -f1)"
115+
if [[ -n "$want" && "$got" != "$want" ]]; then
116+
printf '%b\n' " ${RED}[!] sha256 mismatch for ${name}${RESET}" >&2
117+
printf '%b\n' " want ${want}" >&2
118+
printf '%b\n' " got ${got}" >&2
119+
exit 1
120+
fi
121+
# head -1: grep -o can emit several matches from one binary "line".
122+
local off; off="$(grep -aboFm1 $'PK\x03\x04' "$tmp/e.crx" | head -1 | cut -d: -f1 || true)"
123+
if [[ -z "$off" ]]; then
124+
printf '%b\n' " ${RED}[!] no zip payload in crx ${name}${RESET}" >&2
125+
exit 1
126+
fi
127+
tail -c "+$((off + 1))" "$tmp/e.crx" > "$tmp/e.zip"
128+
mkdir -p "$tmp/x"
129+
unzip -qo "$tmp/e.zip" -d "$tmp/x"
130+
if [[ ! -f "$tmp/x/manifest.json" ]]; then
131+
printf '%b\n' " ${RED}[!] no manifest.json in crx ${name}${RESET}" >&2
132+
exit 1
133+
fi
134+
rm -rf "${dir:?}/$name"
135+
mkdir -p "$dir/$name"
136+
cp -a "$tmp/x/." "$dir/$name/"
137+
echo "$ver" > "$dir/$name/.dotsec-version"
138+
printf '%b\n' " ${GREEN}[+]${RESET} ${name} ${DIM}(crx ${ver})${RESET}"
139+
)
140+
}
141+
142+
# __ext_sync_one <name> <provider> <source> <ref> <sha256> <subdir>
143+
__ext_sync_one() {
144+
local name="$1" provider="$2" src="$3" ref="$4" sha="$5" subdir="$6"
145+
if [[ -n "${__EXT_ONLY:-}" && "${__EXT_ONLY}" != "$name" ]]; then
146+
return 0
147+
fi
148+
local marker; marker="$(__ext_dir)/$name/.dotsec-version"
149+
if [[ -f "$marker" ]] && [[ "$(cat "$marker")" == "$ref" ]]; then
150+
printf '%b\n' " ${DIM}= ${name} (up-to-date ${ref})${RESET}"
151+
return 0
152+
fi
153+
# One failing extension must not abort the whole sync; the error is printed.
154+
case "$provider" in
155+
github) __ext_fetch_github "$name" "$src" "$ref" "$sha" "$subdir" || true;;
156+
webstore) __ext_fetch_webstore "$name" "$src" "$ref" "$sha" || true;;
157+
*) printf '%b\n' " ${RED}[!] unknown provider '${provider}' for ${name}${RESET}" >&2;;
158+
esac
159+
}
160+
161+
# ext_sync [only]: install all (or one named) manifest entries into the ext dir.
162+
ext_sync() {
163+
local only="${1:-}"
164+
local dir; dir="$(__ext_dir)"
165+
mkdir -p "$dir"
166+
printf '%b\n' "${YELLOW}[*]${RESET} Syncing extensions into ${CYAN}${dir}${RESET}"
167+
__EXT_ONLY="$only"
168+
__ext_each __ext_sync_one
169+
unset __EXT_ONLY
170+
}
171+
172+
cmd_ext() {
173+
case "${1:-}" in
174+
list) shift; ext_list "$@";;
175+
sync) shift; ext_sync "$@";;
176+
*) printf '%b\n' "${RED}[!] dotsec ext sync|list${RESET}" >&2; return 1;;
177+
esac
178+
}

lib/proxy.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,28 @@ cmd_browser() {
139139
flags+=" --disable-gpu --ozone-platform=x11"
140140
fi
141141

142+
# Extensions (runtime, managed by `dotsec ext sync`) + Chromium managed
143+
# policies (favourites + bookmark bar). The entrypoint writes a second
144+
# policy file that pins the extensions and grants them all-hosts access.
145+
local extra=()
146+
local ext_dir="${DOTSEC_EXT_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/dotenvsec/extensions}"
147+
if [[ -d "$ext_dir" ]] && [[ -n "$(ls -A "$ext_dir" 2>/dev/null)" ]]; then
148+
extra+=(-v "${ext_dir}:/extensions:ro")
149+
fi
150+
local pol="${DOTSEC_HOME}/chromium/managed-policies.json"
151+
local user_pol="${XDG_CONFIG_HOME:-$HOME/.config}/dotenvsec/policies.json"
152+
if [[ -f "$user_pol" ]]; then pol="$user_pol"; fi
153+
if [[ -f "$pol" ]]; then
154+
extra+=(-v "${pol}:/etc/chromium/policies/managed/dotsec.json:ro")
155+
fi
156+
142157
docker run --rm \
143158
--network dotsec-proxy-net \
144159
-e HTTP_PROXY="http://${proxy_host}:8080" \
145160
-e HTTPS_PROXY="http://${proxy_host}:8080" \
146161
-e CHROMIUM_FLAGS="${flags}" \
147162
"${display[@]}" \
163+
"${extra[@]}" \
148164
-v "${ws}/proxy/certs:/certs:ro" \
149165
-p 127.0.0.1:9222:9222 \
150166
dotenv-sec/chromium:latest

0 commit comments

Comments
 (0)