Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@

Newest first. One line per change. Default-behavior changes marked **CHANGED**. Package versions in brackets.

## 2026-10-05

### Skills
- Added `skills/create-skill/SKILL.md`: write, check and publish an agent skill (a folder with a `SKILL.md`). Covers frontmatter, privacy and secret checks, third-party content, QA and the publish steps.
- Added `skills/skill-publish-review/SKILL.md`: review agent skills before they go into a public repository. Finds leaked personal or internal detail, broken frontmatter, unfinished work and text copied from somewhere else, runs independent reviewers in parallel and merges their verdicts.
- Added `skills/ssrf-safe-fetch/SKILL.md`: write or review server code that fetches a URL it did not get from its own fixed configuration. Validate the scheme, port and resolved address before the request, follow redirects by hand, and what to test.
- `skills/quarantine-review`, `skills/login-item-triage` and `skills/secure-skill-pull` frontmatter now parses as YAML: their descriptions are quoted, so a loader no longer skips them.

### secure-semgrep
- New `rules/ssrf` rules, all warnings: a request whose URL is not a fixed string and does not pass through a guard function; a client that follows redirects automatically for such a URL; and TLS certificate verification turned off (`rejectUnauthorized: false`, `verify=False`, `danger_accept_invalid_certs(true)`). JavaScript/TypeScript, Python and Rust.

## 2026-10-02

### Skills
- Added `skills/third-party-skill-intake/SKILL.md`: vet a third-party agent skill, plugin or setup recipe before using it. Fetch it as data at a pinned commit, scan it, read it, record where it came from, and never run its installer.
- Added `skills/secure-skill-pull/SKILL.md`: vet a third-party agent skill, plugin or setup recipe before using it. Fetch it as data at a pinned commit, scan it, read it, record where it came from, and never run its installer.

## 2026-10-01 [am-i-hacked 2.0.0]

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pnpx am-i-hacked . # or: npx am-i-hacked .
| [`am-i-being-recorded`](apps/am-i-being-recorded/README.md) | Which browser extension is behind a screen-recording indicator, and what else on the machine can capture you. | `pnpx am-i-being-recorded` |
| [`secure-semgrep`](apps/secure-semgrep/README.md) | Semgrep rules and loadout packs for AI-agent, bash and web code. Needs `semgrep`. | From this repository |

Agent skills in [`skills/`](skills/): [quarantine-review](skills/quarantine-review/SKILL.md) (inspect an untrusted repository without running any of it), [third-party-skill-intake](skills/third-party-skill-intake/SKILL.md) (vet a skill or plugin from a URL without running its installer), [login-item-triage](skills/login-item-triage/SKILL.md) and [jujutsu](skills/jujutsu/SKILL.md).
Agent skills in [`skills/`](skills/): [quarantine-review](skills/quarantine-review/SKILL.md) (inspect an untrusted repository without running any of it), [secure-skill-pull](skills/secure-skill-pull/SKILL.md) (vet a skill or plugin from a URL without running its installer), [create-skill](skills/create-skill/SKILL.md) (write, check and publish an agent skill), [skill-publish-review](skills/skill-publish-review/SKILL.md) (review skills before they go into a public repository), [ssrf-safe-fetch](skills/ssrf-safe-fetch/SKILL.md) (validate the URL and the resolved address before a server fetches it), [login-item-triage](skills/login-item-triage/SKILL.md) and [jujutsu](skills/jujutsu/SKILL.md).

## Why this one?

Expand Down
25 changes: 25 additions & 0 deletions apps/secure-semgrep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Bundled (in this package, under `rules/`) — you own and extend these:
abuse, and more (Python / TypeScript / Go / Java / Ruby / others).
- `rules/bash` — bash security & correctness: `IFS` tampering, unsafe `curl |
bash`, `curl ... | eval`, and unquoted-expansion footguns.
- `rules/ssrf` — server-side request forgery (SSRF): a request whose URL is not
a fixed string and does not pass through a guard function, a client that
follows redirects automatically for such a URL, and TLS certificate
verification turned off. JavaScript / TypeScript / Python / Rust.

Loadouts (pulled from Semgrep's registry at scan time, so you don't vendor
them): `py`, `js`, `ts`, `react`, `node`, `rust`.
Expand All @@ -33,6 +37,27 @@ them): `py`, `js`, `ts`, `react`, `node`, `rust`.
> bloats this package and collides with the packs. You own the *bespoke* rules;
> you *compose* the maintained ones.

## SSRF rules

The `rules/ssrf` bundle flags server code that fetches a URL it did not get
from its own fixed configuration, the shape that lets an attacker point the
server at its private network or the cloud metadata address. It covers three
patterns, each across JavaScript/TypeScript, Python and Rust:

- **Unchecked URL** — `fetch`, `axios`, `got`, `requests`, `httpx`, `urllib`,
`reqwest` and similar called with a URL that is not a fixed string and has
not passed through a guard function.
- **Automatic redirects** — a client that follows redirects for a URL that is
not fixed and not guarded, so an allowed host can redirect to a forbidden one.
- **TLS verification disabled** — `rejectUnauthorized: false`,
`verify=False`, or `danger_accept_invalid_certs(true)`.

Every rule is a `WARNING`: it reports, it does not block. A finding is a
question, not a verdict — a non-literal URL can still be safe when it comes
from the server's own fixed configuration. To fix and to test, see the
[`ssrf-safe-fetch`](https://github.com/IsaacBell/secure-devtools/blob/main/skills/ssrf-safe-fetch/SKILL.md)
skill.

## Requirements

| Dependency | Needed for | Install |
Expand Down
2 changes: 2 additions & 0 deletions apps/secure-semgrep/bin/secure-semgrep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Pass extra semgrep flags after -- (scan only):
LOADOUTS (bundled rules ai + bash always run):
ai AI-agent / LLM-provider best-practice rules (bundled)
bash Bash security & correctness rules (bundled)
ssrf Server-side request forgery rules: unchecked URLs, auto redirects, TLS checks off (bundled, opt-in)
py Semgrep p/python
js Semgrep p/javascript
ts Semgrep p/typescript
Expand Down Expand Up @@ -79,6 +80,7 @@ append_loadout() {
case "$name" in
ai) configs+=("${RULES_DIR}/ai") ;;
bash) configs+=("${RULES_DIR}/bash") ;;
ssrf) configs+=("${RULES_DIR}/ssrf") ;;
py) configs+=("p/python") ;;
js) configs+=("p/javascript") ;;
ts) configs+=("p/typescript") ;;
Expand Down
34 changes: 34 additions & 0 deletions apps/secure-semgrep/rules/ssrf/ssrf-auto-redirect-javascript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import axios from "axios";
import got from "got";

import { assertPublicUrl, safeFetch } from "./net";

async function fetchFollowingRedirects(url: string) {
// ruleid: ssrf-auto-redirect-javascript
return axios(url, { maxRedirects: 5 });
}

async function fetchFollowingRedirectsGot(url: string) {
// ruleid: ssrf-auto-redirect-javascript-got
return got(url, { followRedirect: true });
}

async function fetchFixedUrlFollowingRedirects() {
// ok: ssrf-auto-redirect-javascript
return axios("https://api.example.com/v1/status", { maxRedirects: 5 });
}

async function fetchGuardedFollowingRedirects(url: string) {
// ok: ssrf-auto-redirect-javascript
return axios(assertPublicUrl(url), { maxRedirects: 5 });
}

async function fetchNoAutoRedirect(url: string) {
// ok: ssrf-auto-redirect-javascript
return axios(url, { maxRedirects: 0 });
}

async function fetchViaHelper(url: string) {
// ok: ssrf-auto-redirect-javascript
return safeFetch(url);
}
76 changes: 76 additions & 0 deletions apps/secure-semgrep/rules/ssrf/ssrf-auto-redirect-javascript.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
rules:
- id: ssrf-auto-redirect-javascript
languages: [js, ts]
severity: WARNING
message: >-
This request is explicitly configured to follow redirects automatically
(`maxRedirects` greater than 0) for a URL that is not a fixed string and
is not checked by a guard function. A server that is allowed can redirect
the request to one that is not (its own private network or cloud metadata
address). Turn off automatic redirects (`maxRedirects: 0`) and check
each hop by hand before following it. This rule only flags an explicit
`maxRedirects` option; a call that omits it is not caught here. See the
ssrf-safe-fetch skill.
metadata:
cwe: "CWE-918: Server-Side Request Forgery (SSRF)"
owasp:
- "A10:2021 - Server-Side Request Forgery"
category: security
subcategory: [vuln]
confidence: MEDIUM
likelihood: LOW
impact: HIGH
technology: [javascript, typescript, node, axios, got]
references:
- https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
patterns:
- pattern-either:
- pattern: "axios($URL, { ..., maxRedirects: $N, ... })"
- pattern: "axios.get($URL, { ..., maxRedirects: $N, ... })"
- pattern: "axios.post($URL, $DATA, { ..., maxRedirects: $N, ... })"
- metavariable-pattern:
metavariable: $N
patterns:
- pattern-not: "0"
- metavariable-pattern:
metavariable: $URL
patterns:
- pattern-not: "\"...\""
- pattern-not: "'...'"
- pattern-not: checkPublicUrl(...)
- pattern-not: assertPublicUrl(...)
- pattern-not: safeFetch(...)
- id: ssrf-auto-redirect-javascript-got
languages: [js, ts]
severity: WARNING
message: >-
This request is explicitly configured to follow redirects automatically
(`followRedirect: true`) for a URL that is not a fixed string and is not
checked by a guard function. A server that is allowed can redirect the
request to one that is not (its own private network or cloud metadata
address). Turn off automatic redirects (`followRedirect: false`, or
`redirect: { followRedirect: false }`) and check each hop by hand before
following it. This rule only flags an explicit `followRedirect: true`; a
call that omits it is not caught here. See the ssrf-safe-fetch skill.
metadata:
cwe: "CWE-918: Server-Side Request Forgery (SSRF)"
owasp:
- "A10:2021 - Server-Side Request Forgery"
category: security
subcategory: [vuln]
confidence: MEDIUM
likelihood: LOW
impact: HIGH
technology: [javascript, typescript, node, got]
references:
- https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
patterns:
- pattern: "got($URL, { ..., followRedirect: true, ... })"
- metavariable-pattern:
metavariable: $URL
patterns:
- pattern-not: "\"...\""
- pattern-not: "'...'"
- pattern-not: checkPublicUrl(...)
- pattern-not: assertPublicUrl(...)
- pattern-not: safeFetch(...)
34 changes: 34 additions & 0 deletions apps/secure-semgrep/rules/ssrf/ssrf-auto-redirect-python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import httpx
import requests

from myapp.net import assert_public_url, safe_fetch


def fetch_following_redirects(url):
# ruleid: ssrf-auto-redirect-python
return requests.get(url, allow_redirects=True)


def fetch_following_redirects_httpx(url):
# ruleid: ssrf-auto-redirect-python
return httpx.get(url, follow_redirects=True)


def fetch_fixed_url_following_redirects():
# ok: ssrf-auto-redirect-python
return requests.get("https://api.example.com/v1/status", allow_redirects=True)


def fetch_guarded_following_redirects(url):
# ok: ssrf-auto-redirect-python
return requests.get(assert_public_url(url), allow_redirects=True)


def fetch_no_auto_redirect(url):
# ok: ssrf-auto-redirect-python
return requests.get(url, allow_redirects=False)


def fetch_via_helper(url):
# ok: ssrf-auto-redirect-python
return safe_fetch(url)
42 changes: 42 additions & 0 deletions apps/secure-semgrep/rules/ssrf/ssrf-auto-redirect-python.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
rules:
- id: ssrf-auto-redirect-python
languages: [python]
severity: WARNING
message: >-
This request is explicitly configured to follow redirects automatically
(`allow_redirects=True` / `follow_redirects=True`) for a URL that is not
a fixed string and is not checked by a guard function. A server that is
allowed can redirect the request to one that is not (its own private
network or cloud metadata address). Turn off automatic redirects
(`allow_redirects=False` / `follow_redirects=False`) and check each hop
by hand before following it. This rule only flags an explicit
`allow_redirects=True` / `follow_redirects=True` (and the httpx default
is already `False`); a requests call that omits the option follows
redirects by default but is not caught here. See the ssrf-safe-fetch
skill.
metadata:
cwe: "CWE-918: Server-Side Request Forgery (SSRF)"
owasp:
- "A10:2021 - Server-Side Request Forgery"
category: security
subcategory: [vuln]
confidence: MEDIUM
likelihood: LOW
impact: HIGH
technology: [python, requests, httpx]
references:
- https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
patterns:
- pattern-either:
- pattern: requests.get($URL, ..., allow_redirects=True, ...)
- pattern: requests.request($METHOD, $URL, ..., allow_redirects=True, ...)
- pattern: httpx.get($URL, ..., follow_redirects=True, ...)
- pattern: httpx.request($METHOD, $URL, ..., follow_redirects=True, ...)
- metavariable-pattern:
metavariable: $URL
patterns:
- pattern-not: "\"...\""
- pattern-not: f"..."
- pattern-not: check_public_url(...)
- pattern-not: assert_public_url(...)
- pattern-not: safe_fetch(...)
15 changes: 15 additions & 0 deletions apps/secure-semgrep/rules/ssrf/ssrf-auto-redirect-rust.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
fn client_following_redirects() -> reqwest::Client {
// ruleid: ssrf-auto-redirect-rust
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.unwrap()
}

fn client_no_auto_redirect() -> reqwest::Client {
// ok: ssrf-auto-redirect-rust
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap()
}
28 changes: 28 additions & 0 deletions apps/secure-semgrep/rules/ssrf/ssrf-auto-redirect-rust.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
rules:
- id: ssrf-auto-redirect-rust
languages: [rust]
severity: WARNING
message: >-
This client is explicitly built with a redirect policy that follows a
bounded number of hops (`redirect(Policy::limited(..))`) and may not
re-check each hop's address. A server that is allowed can redirect the
request to one that is not (its own private network or cloud metadata
address). Build the client with
`.redirect(reqwest::redirect::Policy::none())` and check each hop by
hand before following it. This rule only flags an explicit
`Policy::limited(..)`; a builder that never calls `.redirect(...)` (which
also follows redirects by default) is not caught here. See the
ssrf-safe-fetch skill.
metadata:
cwe: "CWE-918: Server-Side Request Forgery (SSRF)"
owasp:
- "A10:2021 - Server-Side Request Forgery"
category: security
subcategory: [vuln]
confidence: LOW
likelihood: LOW
impact: HIGH
technology: [rust, reqwest]
references:
- https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
pattern: $BUILDER.redirect(reqwest::redirect::Policy::limited($N))
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import https from "node:https";
import axios from "axios";

function insecureAgent() {
// ruleid: ssrf-tls-verification-disabled-javascript
return new https.Agent({ rejectUnauthorized: false });
}

function insecureAxiosInstance() {
// ruleid: ssrf-tls-verification-disabled-javascript
return axios.create({ rejectUnauthorized: false, timeout: 5000 });
}

function secureAgent() {
// ok: ssrf-tls-verification-disabled-javascript
return new https.Agent({ rejectUnauthorized: true });
}

function secureAxiosInstance() {
// ok: ssrf-tls-verification-disabled-javascript
return axios.create({ timeout: 5000 });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
rules:
- id: ssrf-tls-verification-disabled-javascript
languages: [js, ts]
severity: WARNING
message: >-
TLS certificate verification is turned off (`rejectUnauthorized:
false`). Without it, a network attacker, or a redirect to an address
the attacker controls, can intercept or redirect the request
unnoticed. Remove it; if a private CA is the real problem, pass its
certificate with the `ca` option instead. See the ssrf-safe-fetch
skill.
metadata:
cwe: "CWE-295: Improper Certificate Validation"
owasp:
- "A02:2021 - Cryptographic Failures"
category: security
subcategory: [vuln]
confidence: HIGH
likelihood: LOW
impact: HIGH
technology: [javascript, typescript, node, axios, got, https]
references:
- https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
pattern: "{ ..., rejectUnauthorized: false, ... }"
Loading
Loading