mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-29 08:01:00 +00:00
Translated ['pentesting-web/browser-extension-pentesting-methodology/REA
This commit is contained in:
parent
d8876f8359
commit
1279cfd018
1 changed files with 130 additions and 67 deletions
|
@ -16,7 +16,7 @@ Andere Möglichkeiten, HackTricks zu unterstützen:
|
|||
|
||||
## Grundlegende Informationen
|
||||
|
||||
Browser-Erweiterungen sind in JavaScript geschrieben und werden vom Browser im Hintergrund geladen. Es hat sein [DOM](https://www.w3schools.com/js/js\_htmldom.asp), kann jedoch mit DOMs anderer Websites interagieren. Dies bedeutet, dass es die Vertraulichkeit, Integrität und Verfügbarkeit (CIA) anderer Websites beeinträchtigen kann.
|
||||
Browser-Erweiterungen werden in JavaScript geschrieben und vom Browser im Hintergrund geladen. Sie haben ihr [DOM](https://www.w3schools.com/js/js\_htmldom.asp), können jedoch mit DOMs anderer Websites interagieren. Dies bedeutet, dass sie die Vertraulichkeit, Integrität und Verfügbarkeit (CIA) anderer Websites beeinträchtigen können.
|
||||
|
||||
## Hauptkomponenten
|
||||
|
||||
|
@ -34,17 +34,17 @@ Der Erweiterungskern enthält die meisten Erweiterungsprivilegien/-zugriffe, kan
|
|||
|
||||
### **Native Binärdatei**
|
||||
|
||||
Die Erweiterung ermöglicht eine native Binärdatei, die **mit den vollen Berechtigungen des Benutzers auf den Hostrechner zugreifen kann**. Die native Binärdatei interagiert mit dem Erweiterungskern über die Standard-Netscape-Plugin-Anwendungsprogrammierschnittstelle ([NPAPI](https://en.wikipedia.org/wiki/NPAPI)), die von Flash und anderen Browser-Plug-Ins verwendet wird.
|
||||
Die Erweiterung ermöglicht eine native Binärdatei, die **mit den vollen Benutzerrechten auf den Hostrechner zugreifen kann**. Die native Binärdatei interagiert mit dem Erweiterungskern über die Standard-Netscape-Plugin-Anwendungsprogrammierschnittstelle ([NPAPI](https://en.wikipedia.org/wiki/NPAPI)), die von Flash und anderen Browser-Plug-Ins verwendet wird.
|
||||
|
||||
### Grenzen
|
||||
|
||||
{% hint style="danger" %}
|
||||
Um die vollen Berechtigungen des Benutzers zu erlangen, muss ein Angreifer die Erweiterung überzeugen, bösartige Eingaben vom Inhaltsskript an den Erweiterungskern und vom Erweiterungskern an die native Binärdatei weiterzuleiten.
|
||||
Um die vollen Benutzerrechte zu erlangen, muss ein Angreifer die Erweiterung überzeugen, bösartige Eingaben vom Inhaltsskript an den Erweiterungskern und vom Erweiterungskern an die native Binärdatei weiterzuleiten.
|
||||
{% endhint %}
|
||||
|
||||
Jede Komponente der Erweiterung ist durch **starke Schutzgrenzen** voneinander getrennt. Jede Komponente läuft in einem **eigenen Betriebssystemprozess**. Inhaltsskripte und Erweiterungskerne laufen in **Sandbox-Prozessen**, die für die meisten Betriebssystemdienste nicht verfügbar sind.
|
||||
Jede Komponente der Erweiterung ist durch **starke Schutzgrenzen voneinander getrennt**. Jede Komponente läuft in einem **eigenen Betriebssystemprozess**. Inhaltsskripte und Erweiterungskerne laufen in **Sandbox-Prozessen**, die für die meisten Betriebssystemdienste nicht verfügbar sind.
|
||||
|
||||
Darüber hinaus sind Inhaltsskripte von ihren zugehörigen Webseiten durch **Ausführen in einem separaten JavaScript-Heap** getrennt. Das Inhaltsskript und die Webseite haben **Zugriff auf denselben zugrunde liegenden DOM**, aber die beiden **tauschen niemals JavaScript-Pointer aus**, was das Auslaufen von JavaScript-Funktionalität verhindert.
|
||||
Darüber hinaus sind Inhaltsskripte von ihren zugehörigen Webseiten durch **Ausführung in einem separaten JavaScript-Heap** getrennt. Das Inhaltsskript und die Webseite haben **Zugriff auf dasselbe zugrunde liegende DOM**, aber die beiden **tauschen niemals JavaScript-Pointer aus**, was das Auslaufen von JavaScript-Funktionalität verhindert.
|
||||
|
||||
## **`manifest.json`**
|
||||
|
||||
|
@ -83,7 +83,7 @@ Beispiel:
|
|||
```
|
||||
### `content_scripts`
|
||||
|
||||
Content-Skripte werden **geladen**, wenn der Benutzer zu einer übereinstimmenden Seite **navigiert**, in unserem Fall zu einer Seite, die dem Ausdruck **`https://example.com/*`** entspricht und nicht dem Regex **`*://*/*/business*`** entspricht. Sie werden **wie die eigenen Skripte der Seite** ausgeführt und haben beliebigen Zugriff auf das [Document Object Model (DOM)](https://developer.mozilla.org/en-US/docs/Web/API/Document\_Object\_Model).
|
||||
Content-Skripte werden **geladen**, wenn der Benutzer zu einer übereinstimmenden Seite **navigiert**, in unserem Fall zu einer Seite, die dem Ausdruck **`https://example.com/*`** entspricht und nicht dem **`*://*/*/business*`**-Regex entspricht. Sie werden **wie die eigenen Skripte der Seite** ausgeführt und haben beliebigen Zugriff auf das [Document Object Model (DOM)](https://developer.mozilla.org/en-US/docs/Web/API/Document\_Object\_Model).
|
||||
```json
|
||||
"content_scripts": [
|
||||
{
|
||||
|
@ -98,7 +98,7 @@ Content-Skripte werden **geladen**, wenn der Benutzer zu einer übereinstimmende
|
|||
}
|
||||
],
|
||||
```
|
||||
Um mehr URLs einzuschließen oder auszuschließen, ist es auch möglich, **`include_globs`** und **`exclude_globs`** zu verwenden.
|
||||
Um weitere URLs einzuschließen oder auszuschließen, ist es auch möglich, **`include_globs`** und **`exclude_globs`** zu verwenden.
|
||||
|
||||
Dies ist ein Beispielinhaltsskript, das eine Erklärungsschaltfläche zur Seite hinzufügt, wenn [die Speicher-API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage) verwendet wird, um den Wert `message` aus dem Speicher der Erweiterung abzurufen.
|
||||
```js
|
||||
|
@ -115,16 +115,16 @@ document.body.appendChild(div);
|
|||
```
|
||||
<figure><img src="../../.gitbook/assets/image (23).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
Eine Nachricht wird an die Erweiterungsseiten gesendet, wenn auf diese Schaltfläche geklickt wird, durch die Nutzung der [**runtime.sendMessage() API**](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage). Dies liegt an der Beschränkung des Inhaltskripts beim direkten Zugriff auf APIs, wobei `storage` zu den wenigen Ausnahmen gehört. Für Funktionalitäten über diese Ausnahmen hinaus werden Nachrichten an Erweiterungsseiten gesendet, mit denen Inhaltskripte kommunizieren können.
|
||||
Eine Nachricht wird an die Erweiterungsseiten gesendet, wenn auf diese Schaltfläche geklickt wird, durch die Verwendung der [**runtime.sendMessage() API**](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage). Dies liegt an der Beschränkung des Inhaltskripts beim direkten Zugriff auf APIs, wobei `storage` zu den wenigen Ausnahmen gehört. Für Funktionalitäten über diese Ausnahmen hinaus werden Nachrichten an Erweiterungsseiten gesendet, mit denen Inhaltskripte kommunizieren können.
|
||||
|
||||
{% hint style="warning" %}
|
||||
Je nach Browser können sich die Fähigkeiten des Inhaltskripts geringfügig unterscheiden. Für auf Chromium basierende Browser ist die Liste der Fähigkeiten in der [Chrome-Entwicklerdokumentation](https://developer.chrome.com/docs/extensions/mv3/content_scripts/#capabilities) verfügbar, und für Firefox dient die [MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#webextension_apis) als primäre Quelle.\
|
||||
Es ist auch erwähnenswert, dass Inhaltskripte die Möglichkeit haben, mit Hintergrundskripten zu kommunizieren, was es ihnen ermöglicht, Aktionen auszuführen und Antworten zurückzugeben.
|
||||
{% endhint %}
|
||||
|
||||
Um Inhaltskripte in Chrome anzuzeigen und zu debuggen, kann das Chrome-Entwicklertools-Menü über Optionen > Weitere Tools > Entwicklertools aufgerufen werden ODER durch Drücken von Strg + Umschalt + I.
|
||||
Um Inhaltskripte in Chrome anzuzeigen und zu debuggen, kann das Chrome-Entwicklertools-Menü über Optionen > Weitere Tools > Entwicklertools aufgerufen werden oder durch Drücken von Strg + Umschalt + I.
|
||||
|
||||
Nachdem die Entwicklertools angezeigt werden, sollte auf den **Quellentab** geklickt werden, gefolgt vom **Inhaltskripte**-Tab. Dies ermöglicht die Beobachtung der ausgeführten Inhaltskripte aus verschiedenen Erweiterungen und das Setzen von Haltepunkten, um den Ausführungsfluss zu verfolgen.
|
||||
Nachdem die Entwicklertools angezeigt werden, sollte auf den **Quellcode-Tab** geklickt werden, gefolgt vom **Inhaltskripte-Tab**. Dies ermöglicht die Beobachtung der ausgeführten Inhaltskripte aus verschiedenen Erweiterungen und das Setzen von Haltepunkten, um den Ausführungsfluss zu verfolgen.
|
||||
|
||||
### Eingefügte Inhaltskripte
|
||||
|
||||
|
@ -168,7 +168,7 @@ files: ["content-script.js"]
|
|||
});
|
||||
});
|
||||
```
|
||||
* **Fügen Sie eine Funktion hinzu** beim Klicken:
|
||||
* **Fügen Sie eine Funktion** beim Klicken hinzu:
|
||||
```javascript
|
||||
//service-worker.js - Inject a function
|
||||
function injectedFunction() {
|
||||
|
@ -197,7 +197,7 @@ chrome.tabs.executeScript(tabId, { file: "content_script.js" });
|
|||
```
|
||||
Um mehr URLs einzuschließen oder auszuschließen, ist es auch möglich, **`include_globs`** und **`exclude_globs`** zu verwenden.
|
||||
|
||||
### Inhalts-Scripts `run_at`
|
||||
### Inhalts Skripte `run_at`
|
||||
|
||||
Das Feld `run_at` steuert **wann JavaScript-Dateien in die Webseite eingefügt werden**. Der bevorzugte und Standardwert ist `"document_idle"`.
|
||||
|
||||
|
@ -234,19 +234,19 @@ js : [ "contentScript.js" ],
|
|||
```
|
||||
### `Hintergrund`
|
||||
|
||||
Nachrichten, die von Inhaltsskripten gesendet werden, werden von der **Hintergrundseite** empfangen, die eine zentrale Rolle bei der Koordination der Komponenten der Erweiterung spielt. Insbesondere bleibt die Hintergrundseite während der gesamten Lebensdauer der Erweiterung bestehen und arbeitet diskret ohne direkte Benutzerinteraktion. Sie verfügt über ihr eigenes Document Object Model (DOM), das komplexe Interaktionen und Zustandsverwaltung ermöglicht.
|
||||
Nachrichten, die von Inhaltsskripten gesendet werden, werden von der **Hintergrundseite** empfangen, die eine zentrale Rolle bei der Koordination der Komponenten der Erweiterung spielt. Insbesondere bleibt die Hintergrundseite während der gesamten Lebensdauer der Erweiterung bestehen, arbeitet diskret ohne direkte Benutzerinteraktion und verfügt über ihr eigenes Document Object Model (DOM), das komplexe Interaktionen und Zustandsverwaltung ermöglicht.
|
||||
|
||||
**Hauptpunkte**:
|
||||
**Schlüsselpunkte**:
|
||||
|
||||
- **Rolle der Hintergrundseite:** Dient als Nervenzentrum für die Erweiterung und gewährleistet die Kommunikation und Koordination zwischen verschiedenen Teilen der Erweiterung.
|
||||
- **Persistenz:** Es handelt sich um eine ständig vorhandene Entität, die für den Benutzer unsichtbar ist, aber für die Funktionalität der Erweiterung unerlässlich ist.
|
||||
- **Automatische Generierung:** Wenn nicht explizit definiert, wird der Browser automatisch eine Hintergrundseite erstellen. Diese automatisch generierte Seite enthält alle im Manifest der Erweiterung angegebenen Hintergrundskripte und gewährleistet den nahtlosen Betrieb der Hintergrundaufgaben der Erweiterung.
|
||||
- **Automatische Generierung:** Wenn nicht explizit definiert, wird vom Browser automatisch eine Hintergrundseite erstellt. Diese automatisch generierte Seite enthält alle im Manifest der Erweiterung angegebenen Hintergrundskripte und gewährleistet den nahtlosen Betrieb der Hintergrundaufgaben der Erweiterung.
|
||||
|
||||
{% hint style="success" %}
|
||||
Die Bequemlichkeit, die der Browser durch die automatische Generierung einer Hintergrundseite bietet (wenn nicht explizit deklariert), stellt sicher, dass alle erforderlichen Hintergrundskripte integriert und betriebsbereit sind, was den Einrichtungsprozess der Erweiterung vereinfacht.
|
||||
Die Bequemlichkeit, die der Browser durch die automatische Generierung einer Hintergrundseite bietet (wenn nicht explizit deklariert), stellt sicher, dass alle erforderlichen Hintergrundskripte integriert und betriebsbereit sind, was den Einrichtungsprozess der Erweiterung optimiert.
|
||||
{% endhint %}
|
||||
|
||||
Beispiel Hintergrundskript:
|
||||
Beispielhintergrundskript:
|
||||
```js
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) =>
|
||||
{
|
||||
|
@ -272,7 +272,7 @@ Browser-Erweiterungen können verschiedene Arten von Seiten enthalten:
|
|||
|
||||
<figure><img src="../../.gitbook/assets/image (24).png" alt="" width="375"><figcaption></figcaption></figure>
|
||||
|
||||
Beachten Sie, dass diese Seiten im Gegensatz zu Hintergrundseiten nicht persistent sind, da sie dynamisch Inhalte bei Bedarf laden. Trotzdem teilen sie bestimmte Fähigkeiten mit der Hintergrundseite:
|
||||
Beachten Sie, dass diese Seiten im Gegensatz zu Hintergrundseiten nicht persistent sind, da sie bei Bedarf dynamisch Inhalte laden. Trotzdem teilen sie bestimmte Fähigkeiten mit der Hintergrundseite:
|
||||
|
||||
* **Kommunikation mit Inhaltskripten:** Ähnlich wie die Hintergrundseite können diese Seiten Nachrichten von Inhaltskripten empfangen, um die Interaktion innerhalb der Erweiterung zu erleichtern.
|
||||
* **Zugriff auf erweiterungsspezifische APIs:** Diese Seiten haben umfassenden Zugriff auf erweiterungsspezifische APIs, abhängig von den für die Erweiterung definierten Berechtigungen.
|
||||
|
@ -283,7 +283,7 @@ Beachten Sie, dass diese Seiten im Gegensatz zu Hintergrundseiten nicht persiste
|
|||
|
||||
Da Browser-Erweiterungen so **privilegiert** sein können, könnte eine bösartige oder kompromittierte Erweiterung einem Angreifer **verschiedene Möglichkeiten bieten, sensible Informationen zu stehlen und den Benutzer auszuspionieren**.
|
||||
|
||||
Überprüfen Sie, wie diese Einstellungen funktionieren und wie sie missbraucht werden können in:
|
||||
Überprüfen Sie, wie diese Einstellungen funktionieren und wie sie missbraucht werden könnten in:
|
||||
|
||||
{% content-ref url="browext-permissions-and-host_permissions.md" %}
|
||||
[browext-permissions-and-host\_permissions.md](browext-permissions-and-host\_permissions.md)
|
||||
|
@ -297,7 +297,7 @@ Die Standardkonfiguration für Browser-Erweiterungsseiten ist eher restriktiv:
|
|||
```bash
|
||||
script-src 'self'; object-src 'self';
|
||||
```
|
||||
Für weitere Informationen zu CSP und möglichen Umgehungen überprüfen Sie:
|
||||
Für weitere Informationen zu CSP und möglichen Umgehungen siehe:
|
||||
|
||||
{% content-ref url="../content-security-policy-csp-bypass/" %}
|
||||
[content-security-policy-csp-bypass](../content-security-policy-csp-bypass/)
|
||||
|
@ -331,25 +331,35 @@ In öffentlichen Erweiterungen ist die **Erweiterungs-ID zugänglich**:
|
|||
|
||||
<figure><img src="../../.gitbook/assets/image (1194).png" alt="" width="375"><figcaption></figcaption></figure>
|
||||
|
||||
Wenn jedoch der Parameter `manifest.json` **`use_dynamic_url`** verwendet wird, kann diese **ID dynamisch sein**.
|
||||
Wenn jedoch der Parameter **`use_dynamic_url`** in der `manifest.json` verwendet wird, kann diese **ID dynamisch sein**.
|
||||
|
||||
Das Zugreifen auf diese Seiten macht sie anfällig für **potenzielles Clickjacking**:
|
||||
{% hint style="success" %}
|
||||
Beachten Sie, dass selbst wenn hier eine Seite erwähnt wird, sie möglicherweise **gegen Clickjacking geschützt** ist, dank der **Content Security Policy**. Daher müssen Sie auch dies überprüfen (Abschnitt frame-ancestors), bevor Sie einen möglichen Clickjacking-Angriff bestätigen.
|
||||
{% endhint %}
|
||||
|
||||
Das Zugreifen auf diese Seiten kann diese Seiten **potenziell anfällig für Clickjacking** machen:
|
||||
|
||||
{% content-ref url="browext-clickjacking.md" %}
|
||||
[browext-clickjacking.md](browext-clickjacking.md)
|
||||
{% endcontent-ref %}
|
||||
|
||||
{% hint style="success" %}
|
||||
Indem man diese Seiten nur von der Erweiterung und nicht von zufälligen URLs laden lässt, könnten Clickjacking-Angriffe verhindert werden.
|
||||
Indem Sie diese Seiten nur von der Erweiterung und nicht von zufälligen URLs laden lassen, könnten Clickjacking-Angriffe verhindert werden.
|
||||
{% endhint %}
|
||||
|
||||
{% hint style="danger" %}
|
||||
Beachten Sie, dass die Seiten aus **`web_accessible_resources`** und andere Seiten der Erweiterung auch in der Lage sind, **Hintergrundskripte zu kontaktieren**. Wenn also eine dieser Seiten anfällig für **XSS** ist, könnte dies eine größere Sicherheitslücke öffnen.
|
||||
|
||||
Außerdem können nur Seiten, die in **`web_accessible_resources`** angegeben sind, in iframes geöffnet werden, aber von einem neuen Tab aus ist es möglich, auf jede Seite in der Erweiterung zuzugreifen, wenn die Erweiterungs-ID bekannt ist. Daher könnte, wenn ein XSS gefunden wird, das dieselben Parameter missbraucht, dies auch missbraucht werden, selbst wenn die Seite nicht in **`web_accessible_resources`** konfiguriert ist.
|
||||
{% endhint %}
|
||||
|
||||
### `externally_connectable`
|
||||
|
||||
Gemäß der [**Dokumentation**](https://developer.chrome.com/docs/extensions/reference/manifest/externally-connectable) gibt die Manifesteigenschaft `"externally_connectable"` an, **welche Erweiterungen und Webseiten** über [runtime.connect](https://developer.chrome.com/docs/extensions/reference/runtime#method-connect) und [runtime.sendMessage](https://developer.chrome.com/docs/extensions/reference/runtime#method-sendMessage) mit Ihrer Erweiterung verbunden werden können.
|
||||
Gemäß der [**Dokumentation**](https://developer.chrome.com/docs/extensions/reference/manifest/externally-connectable) gibt die Eigenschaft `"externally_connectable"` im Manifest an, **welche Erweiterungen und Webseiten** über [runtime.connect](https://developer.chrome.com/docs/extensions/reference/runtime#method-connect) und [runtime.sendMessage](https://developer.chrome.com/docs/extensions/reference/runtime#method-sendMessage) mit Ihrer Erweiterung verbunden werden können.
|
||||
|
||||
* Wenn der Schlüssel **`externally_connectable`** nicht in Ihrem Erweiterungsmanifest deklariert ist oder als **`"ids": ["*"]`** deklariert ist, **können alle Erweiterungen eine Verbindung herstellen, aber keine Webseiten**.
|
||||
* Wenn **spezifische IDs** angegeben sind, wie in `"ids": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]`, **können nur diese Anwendungen** eine Verbindung herstellen.
|
||||
* Wenn **Übereinstimmungen** angegeben sind, können diese Webanwendungen eine Verbindung herstellen:
|
||||
* Wenn der Schlüssel **`externally_connectable`** nicht im Manifest Ihrer Erweiterung deklariert ist oder als **`"ids": ["*"]`** deklariert ist, können **alle Erweiterungen eine Verbindung herstellen, aber keine Webseiten**.
|
||||
* Wenn **spezifische IDs angegeben sind**, wie in `"ids": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]`, können **nur diese Anwendungen** eine Verbindung herstellen.
|
||||
* Wenn **Übereinstimmungen** angegeben sind, können diese Web-Apps eine Verbindung herstellen:
|
||||
```json
|
||||
"matches": [
|
||||
"https://*.google.com/*",
|
||||
|
@ -360,25 +370,75 @@ Gemäß der [**Dokumentation**](https://developer.chrome.com/docs/extensions/ref
|
|||
Je weniger Erweiterungen und URLs hier angegeben sind, desto kleiner wird die Angriffsfläche sein.
|
||||
|
||||
{% hint style="danger" %}
|
||||
Wenn eine Webseite, die anfällig für XSS oder Übernahmen ist, in **`externally_connectable`** angegeben ist, kann ein Angreifer Nachrichten direkt an das Hintergrundskript senden, wodurch das Content-Skript und seine CSP vollständig umgangen werden.
|
||||
Wenn eine Webseite **anfällig für XSS oder Übernahme** ist und in **`externally_connectable`** angegeben ist, kann ein Angreifer **Nachrichten direkt an das Hintergrundskript senden**, wodurch das Content-Skript und seine CSP vollständig umgangen werden.
|
||||
|
||||
Daher handelt es sich hierbei um einen sehr leistungsstarken Bypass.
|
||||
Daher handelt es sich hierbei um einen **sehr leistungsstarken Bypass**.
|
||||
|
||||
Darüber hinaus könnte, wenn der Client eine schädliche Erweiterung installiert, selbst wenn diese nicht berechtigt ist, mit der anfälligen Erweiterung zu kommunizieren, XSS-Daten auf einer erlaubten Webseite einschleusen oder **`WebRequest`**- oder **`DeclarativeNetRequest`**-APIs missbrauchen, um Anfragen auf einer gezielten Domain zu manipulieren und eine Anfrage für eine **JavaScript-Datei** auf einer Seite zu ändern. (Beachten Sie, dass CSP auf der gezielten Seite diese Angriffe verhindern könnte). Diese Idee stammt aus [**diesem Bericht**](https://www.darkrelay.com/post/opera-zero-day-rce-vulnerability).
|
||||
Darüber hinaus, wenn der Client eine schädliche Erweiterung installiert, selbst wenn sie nicht berechtigt ist, mit der anfälligen Erweiterung zu kommunizieren, könnte sie **XSS-Daten in eine erlaubte Webseite einschleusen** oder die **`WebRequest`**- oder **`DeclarativeNetRequest`**-APIs missbrauchen, um Anfragen auf einer gezielten Domain zu manipulieren und eine Anfrage für eine **JavaScript-Datei** einer Seite zu ändern. (Beachten Sie, dass die CSP auf der gezielten Seite diese Angriffe verhindern könnte). Diese Idee stammt aus [**diesem Bericht**](https://www.darkrelay.com/post/opera-zero-day-rce-vulnerability).
|
||||
{% endhint %}
|
||||
|
||||
##
|
||||
## Kommunikationszusammenfassung
|
||||
|
||||
## Web **↔︎** Content-Skript-Kommunikation
|
||||
### Erweiterung <--> Webanwendung
|
||||
|
||||
Die Umgebungen, in denen **Content-Skripte** arbeiten, und in denen die Host-Seiten existieren, sind voneinander **getrennt**, was **Isolation** gewährleistet. Trotz dieser Isolation haben beide die Möglichkeit, mit dem **Document Object Model (DOM)** der Seite zu interagieren, einem gemeinsamen Ressourcenbereich. Damit die Host-Seite mit dem **Content-Skript** kommunizieren kann, oder indirekt mit der Erweiterung über das Content-Skript, muss sie das **DOM** nutzen, das beiden Parteien als Kommunikationskanal zugänglich ist.
|
||||
Um zwischen dem Inhalts-Skript und der Webseite zu kommunizieren, werden normalerweise Nachrichten gepostet. Daher finden Sie in der Webanwendung in der Regel Aufrufe der Funktion **`window.postMessage`** und im Inhalts-Skript Listener wie **`window.addEventListener`**. Beachten Sie jedoch, dass die Erweiterung auch **mit der Webanwendung kommunizieren kann, indem sie eine Post-Nachricht sendet** (und daher sollte die Webseite dies erwarten) oder einfach die Webseite einen neuen Skript laden lassen kann.
|
||||
|
||||
### Post-Nachrichten
|
||||
### Innerhalb der Erweiterung
|
||||
|
||||
{% code title="content-script.js" %}
|
||||
Normalerweise wird die Funktion **`chrome.runtime.sendMessage`** verwendet, um eine Nachricht innerhalb der Erweiterung zu senden (normalerweise vom `background`-Skript behandelt) und um sie zu empfangen und zu verarbeiten, wird ein Listener deklariert, der **`chrome.runtime.onMessage.addListener`** aufruft.
|
||||
|
||||
Es ist auch möglich, **`chrome.runtime.connect()`** zu verwenden, um eine dauerhafte Verbindung anstelle des Sendens einzelner Nachrichten zu haben. Es ist möglich, es zu verwenden, um **Nachrichten zu senden** und **zu empfangen** wie im folgenden Beispiel:
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>chrome.runtime.connect()</code> Beispiel</summary>
|
||||
```javascript
|
||||
var port = chrome.runtime.connect();
|
||||
|
||||
// Listen for messages from the web page
|
||||
window.addEventListener("message", (event) => {
|
||||
// Only accept messages from the same window
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the message type is "FROM_PAGE"
|
||||
if (event.data.type && (event.data.type === "FROM_PAGE")) {
|
||||
console.log("Content script received: " + event.data.text);
|
||||
// Forward the message to the background script
|
||||
port.postMessage({ type: 'FROM_PAGE', text: event.data.text });
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Listen for messages from the background script
|
||||
port.onMessage.addListener(function(msg) {
|
||||
console.log("Content script received message from background script:", msg);
|
||||
// Handle the response message from the background script
|
||||
});
|
||||
```
|
||||
</details>
|
||||
|
||||
Es ist auch möglich, Nachrichten von einem Hintergrundskript an ein Inhaltskript in einem bestimmten Tab zu senden, indem **`chrome.tabs.sendMessage`** aufgerufen wird, wobei die **ID des Tabs** angegeben werden muss, an den die Nachricht gesendet werden soll.
|
||||
|
||||
### Von erlaubten `externally_connectable` zum Browser-Plugin
|
||||
|
||||
**Web-Apps und externe Browser-Erweiterungen, die in der Konfiguration `externally_connectable` zugelassen sind, können Anfragen senden mit:
|
||||
```javascript
|
||||
chrome.runtime.sendMessage(extensionId, ...
|
||||
```
|
||||
Wo es erforderlich ist, die **Erweiterungs-ID** zu erwähnen.
|
||||
|
||||
## Web **↔︎** Inhalts Skript Kommunikation
|
||||
|
||||
Die Umgebungen, in denen **Inhalts Skripte** arbeiten und in denen die Host-Seiten existieren, sind voneinander **getrennt**, um **Isolierung** zu gewährleisten. Trotz dieser Isolierung haben beide die Möglichkeit, mit dem **Document Object Model (DOM)** der Seite zu interagieren, einem gemeinsamen Ressourcenbereich. Damit die Host-Seite mit dem **Inhalts Skript** kommunizieren kann, oder indirekt mit der Erweiterung über das Inhalts Skript, ist es erforderlich, das **DOM** zu nutzen, auf das beide Parteien als Kommunikationskanal zugreifen können.
|
||||
|
||||
### Post Nachrichten
|
||||
|
||||
{% code title="inhaltsskript.js" %}
|
||||
```javascript
|
||||
// This is like "chrome.runtime.sendMessage" but to maintain the connection
|
||||
var port = chrome.runtime.connect();
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
// We only accept messages from ourselves
|
||||
if (event.source !== window) {
|
||||
|
@ -387,6 +447,7 @@ return;
|
|||
|
||||
if (event.data.type && (event.data.type === "FROM_PAGE")) {
|
||||
console.log("Content script received: " + event.data.text);
|
||||
// Forward the message to the background script
|
||||
port.postMessage(event.data.text);
|
||||
}
|
||||
}, false);
|
||||
|
@ -408,9 +469,9 @@ Eine sichere Post-Nachrichten-Kommunikation sollte die Echtheit der empfangenen
|
|||
- Das Inhalts-Skript erwartet möglicherweise eine Nachricht nur, wenn der Benutzer eine Aktion ausführt.
|
||||
- **Ursprungsdomäne**: Erwartet möglicherweise eine Nachricht nur von einer Whitelist von Domänen.
|
||||
- Wenn ein Regex verwendet wird, seien Sie sehr vorsichtig.
|
||||
- **Quelle**: `received_message.source !== window` kann verwendet werden, um zu überprüfen, ob die Nachricht **aus demselben Fenster** stammt, in dem das Inhalts-Skript lauscht.
|
||||
- **Quelle**: `received_message.source !== window` kann verwendet werden, um zu überprüfen, ob die Nachricht **aus demselben Fenster** stammt, in dem das Inhalts-Skript zuhört.
|
||||
|
||||
Die zuvor genannten Überprüfungen könnten selbst dann anfällig sein, also überprüfen Sie auf der folgenden Seite **potenzielle Post-Nachrichten-Umgehungen**:
|
||||
Die zuvor durchgeführten Überprüfungen könnten selbst dann anfällig sein, überprüfen Sie daher auf der folgenden Seite **potenzielle Post-Nachrichten-Umgehungen**:
|
||||
|
||||
{% content-ref url="../postmessage-vulnerabilities/" %}
|
||||
[postmessage-vulnerabilities](../postmessage-vulnerabilities/)
|
||||
|
@ -434,16 +495,6 @@ Ein Beispiel für eine **DOM-basierte XSS zur Kompromittierung einer Browsererwe
|
|||
[browext-xss-example.md](browext-xss-example.md)
|
||||
{% endcontent-ref %}
|
||||
|
||||
## Sensible Informationen im Speicher/Code
|
||||
|
||||
Wenn eine Browsererweiterung **sensible Informationen im Speicher speichert**, könnten diese **ausgelesen** (insbesondere auf Windows-Maschinen) und nach diesen Informationen **gesucht** werden.
|
||||
|
||||
Daher sollte der Speicher der Browsererweiterung **nicht als sicher angesehen** werden und **sensible Informationen** wie Anmeldeinformationen oder mnemonische Phrasen **sollten nicht gespeichert werden**.
|
||||
|
||||
Natürlich sollten **keine sensiblen Informationen im Code** platziert werden, da sie **öffentlich** zugänglich sind.
|
||||
|
||||
Um den Speicher des Browsers auszulesen, könnten Sie den Prozessspeicher **auslesen** oder in den **Einstellungen** der Browsererweiterung auf **`Inspect pop-up`** klicken -> Im Abschnitt **`Memory`** -> **`Take a snaphost`** und **`STRG+F`** zum Suchen sensibler Informationen im Snapshot verwenden.
|
||||
|
||||
## Inhalts-Skript **↔︎** Hintergrundskript-Kommunikation
|
||||
|
||||
Ein Inhalts-Skript kann die Funktionen [**runtime.sendMessage()**](https://developer.chrome.com/docs/extensions/reference/runtime#method-sendMessage) **oder** [**tabs.sendMessage()**](https://developer.chrome.com/docs/extensions/reference/tabs#method-sendMessage) verwenden, um eine **einmalige JSON-serialisierbare** Nachricht zu senden.
|
||||
|
@ -458,7 +509,7 @@ const response = await chrome.runtime.sendMessage({greeting: "hello"});
|
|||
console.log(response);
|
||||
})();
|
||||
```
|
||||
Senden einer Anfrage von der **Erweiterung** (normalerweise ein **Hintergrundskript**). Ein Inhalts-Skript kann die Funktionen verwenden, außer dass Sie angeben müssen, an welchen Tab sie gesendet werden soll. Beispiel, wie man eine Nachricht an das Inhalts-Skript im ausgewählten Tab sendet:
|
||||
Senden einer Anfrage von der **Erweiterung** (normalerweise ein **Hintergrundskript**). Beispiel, wie man eine Nachricht an das Inhalts-Skript im ausgewählten Tab sendet:
|
||||
```javascript
|
||||
// From https://stackoverflow.com/questions/36153999/how-to-send-a-message-between-chrome-extension-popup-and-content-script
|
||||
(async () => {
|
||||
|
@ -468,7 +519,7 @@ const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
|
|||
console.log(response);
|
||||
})();
|
||||
```
|
||||
Auf der **Empfängerseite** müssen Sie einen [**runtime.onMessage**](https://developer.chrome.com/docs/extensions/reference/runtime#event-onMessage) **Ereignislistener** einrichten, um die Nachricht zu verarbeiten. Dies sieht sowohl in einem Inhaltskript als auch in einer Erweiterungsseite gleich aus.
|
||||
Auf der **Empfängerseite** müssen Sie einen [**runtime.onMessage**](https://developer.chrome.com/docs/extensions/reference/runtime#event-onMessage) **Ereignislistener** einrichten, um die Nachricht zu verarbeiten. Dies sieht sowohl in einem Inhalts-Script als auch auf einer Erweiterungsseite gleich aus.
|
||||
```javascript
|
||||
// From https://stackoverflow.com/questions/70406787/javascript-send-message-from-content-js-to-background-js
|
||||
chrome.runtime.onMessage.addListener(
|
||||
|
@ -483,28 +534,40 @@ sendResponse({farewell: "goodbye"});
|
|||
```
|
||||
Im hervorgehobenen Beispiel wurde **`sendResponse()`** synchron ausgeführt. Um den `onMessage`-Ereignisbehandler für die asynchrone Ausführung von `sendResponse()` zu ändern, ist es unerlässlich, `return true;` einzubeziehen.
|
||||
|
||||
Eine wichtige Überlegung ist, dass in Szenarien, in denen mehrere Seiten so eingestellt sind, dass sie `onMessage`-Ereignisse empfangen, **die erste Seite, die `sendResponse()` für ein bestimmtes Ereignis ausführt**, die einzige sein wird, die die Antwort effektiv übermitteln kann. Alle nachfolgenden Antworten auf dasselbe Ereignis werden nicht berücksichtigt.
|
||||
Eine wichtige Überlegung ist, dass in Szenarien, in denen mehrere Seiten darauf eingestellt sind, `onMessage`-Ereignisse zu empfangen, **die erste Seite, die `sendResponse()` für ein bestimmtes Ereignis ausführt**, die einzige sein wird, die die Antwort effektiv liefern kann. Alle nachfolgenden Antworten auf dasselbe Ereignis werden nicht berücksichtigt.
|
||||
|
||||
Bei der Erstellung neuer Erweiterungen sollte der Vorzug Promises gegenüber Callbacks gelten. In Bezug auf die Verwendung von Callbacks wird die `sendResponse()`-Funktion nur dann als gültig betrachtet, wenn sie direkt im synchronen Kontext ausgeführt wird oder wenn der Ereignisbehandler eine asynchrone Operation anzeigt, indem er `true` zurückgibt. Sollten keine der Handler `true` zurückgeben oder wenn die `sendResponse()`-Funktion aus dem Speicher entfernt wird (garbage-collected), wird das mit der `sendMessage()`-Funktion verknüpfte Callback standardmäßig ausgelöst.
|
||||
Bei der Erstellung neuer Erweiterungen sollte der Vorzug Promises gegenüber Callbacks gelten. In Bezug auf die Verwendung von Callbacks wird die `sendResponse()`-Funktion nur dann als gültig betrachtet, wenn sie direkt im synchronen Kontext ausgeführt wird oder wenn der Ereignisbehandler eine asynchrone Operation anzeigt, indem er `true` zurückgibt. Sollten keine der Handler `true` zurückgeben oder wenn die `sendResponse()`-Funktion aus dem Speicher entfernt (garbage-collected) wird, wird das mit der `sendMessage()`-Funktion verknüpfte Callback standardmäßig ausgelöst.
|
||||
|
||||
## Sensible Informationen im Speicher/Code/Clipboard
|
||||
|
||||
Wenn eine Browsererweiterung **sensible Informationen im Speicher speichert**, können diese (insbesondere auf Windows-Maschinen) **ausgelesen** und nach diesen Informationen **gesucht** werden.
|
||||
|
||||
Daher sollte der Speicher der Browsererweiterung **nicht als sicher angesehen** werden und **sensible Informationen** wie Anmeldeinformationen oder mnemonische Phrasen **sollten nicht gespeichert werden**.
|
||||
|
||||
Natürlich sollten **keine sensiblen Informationen im Code** platziert werden, da sie **öffentlich** zugänglich sind.
|
||||
|
||||
Um den Speicher des Browsers auszulesen, können Sie **den Prozessspeicher auslesen** oder in den **Einstellungen** der Browsererweiterung auf **`Inspect pop-up`** klicken -> Im Abschnitt **`Memory`** -> **`Take a snaphost`** und **`STRG+F`** zum Suchen innerhalb des Schnappschusses nach sensiblen Informationen verwenden.
|
||||
|
||||
Darüber hinaus sollten hochsensible Informationen wie mnemonische Schlüssel oder Passwörter **nicht im Zwischenspeicher kopiert werden** (oder zumindest innerhalb weniger Sekunden daraus entfernt werden), da Prozesse, die den Zwischenspeicher überwachen, sie dann abrufen können.
|
||||
|
||||
## Laden einer Erweiterung im Browser
|
||||
|
||||
1. **Browser-Erweiterung herunterladen** & entpacken
|
||||
2. Gehe zu **`chrome://extensions/`** und **aktiviere** den `Entwicklermodus`
|
||||
3. Klicke auf die Schaltfläche **`Entpackte Erweiterung laden`**
|
||||
1. **Laden** Sie die Browsererweiterung herunter und entpacken Sie sie
|
||||
2. Gehen Sie zu **`chrome://extensions/`** und **aktivieren** Sie den `Entwicklermodus`
|
||||
3. Klicken Sie auf die Schaltfläche **`Unverpackte Erweiterung laden`**
|
||||
|
||||
In **Firefox** gehst du zu **`about:debugging#/runtime/this-firefox`** und klickst auf die Schaltfläche **`Temporäre Add-on laden`**.
|
||||
In **Firefox** gehen Sie zu **`about:debugging#/runtime/this-firefox`** und klicken auf die Schaltfläche **`Temporäre Add-on laden`**.
|
||||
|
||||
## Abrufen des Quellcodes aus dem Store
|
||||
|
||||
Der Quellcode einer Chrome-Erweiterung kann auf verschiedene Arten erhalten werden. Im Folgenden sind detaillierte Erklärungen und Anweisungen für jede Option aufgeführt.
|
||||
Der Quellcode einer Chrome-Erweiterung kann auf verschiedene Arten erhalten werden. Im Folgenden finden Sie detaillierte Erklärungen und Anweisungen für jede Option.
|
||||
|
||||
### Erweiterung als ZIP über die Befehlszeile herunterladen
|
||||
|
||||
Der Quellcode einer Chrome-Erweiterung kann als ZIP-Datei über die Befehlszeile heruntergeladen werden. Dies beinhaltet die Verwendung von `curl`, um die ZIP-Datei von einer bestimmten URL abzurufen und dann die Inhalte der ZIP-Datei in ein Verzeichnis zu extrahieren. Hier sind die Schritte:
|
||||
|
||||
1. Ersetze `"extension_id"` durch die tatsächliche ID der Erweiterung.
|
||||
2. Führe die folgenden Befehle aus:
|
||||
1. Ersetzen Sie `"extension_id"` durch die tatsächliche ID der Erweiterung.
|
||||
2. Führen Sie die folgenden Befehle aus:
|
||||
```bash
|
||||
extension_id=your_extension_id # Replace with the actual extension ID
|
||||
curl -L -o "$extension_id.zip" "https://clients2.google.com/service/update2/crx?response=redirect&os=mac&arch=x86-64&nacl_arch=x86-64&prod=chromecrx&prodchannel=stable&prodversion=44.0.2403.130&x=id%3D$extension_id%26uc"
|
||||
|
@ -520,11 +583,11 @@ Eine weitere praktische Methode ist die Verwendung des Chrome Extension Source V
|
|||
|
||||
### Quellcode einer lokal installierten Erweiterung anzeigen
|
||||
|
||||
Lokal installierte Chrome-Erweiterungen können ebenfalls inspiziert werden. So geht's:
|
||||
Auch lokal installierte Chrome-Erweiterungen können inspiziert werden. So geht's:
|
||||
|
||||
1. Greifen Sie auf Ihr lokales Chrome-Profilverzeichnis zu, indem Sie `chrome://version/` besuchen und das Feld "Profilpfad" suchen.
|
||||
2. Navigieren Sie zum `Extensions/`-Unterordner innerhalb des Profilverzeichnisses.
|
||||
3. Dieser Ordner enthält alle installierten Erweiterungen, normalerweise mit ihrem Quellcode in einem lesbaren Format.
|
||||
2. Navigieren Sie zum Unterordner `Extensions/` innerhalb des Profilverzeichnisses.
|
||||
3. Dieser Ordner enthält alle installierten Erweiterungen, in der Regel mit ihrem Quellcode in einem lesbaren Format.
|
||||
|
||||
Um Erweiterungen zu identifizieren, können Sie ihre IDs den Namen zuordnen:
|
||||
|
||||
|
@ -541,7 +604,7 @@ Gehen Sie zum Chrome Web Store und laden Sie die Erweiterung herunter. Die Datei
|
|||
|
||||
## Sicherheitsprüfungs-Checkliste
|
||||
|
||||
Obwohl Browser-Erweiterungen eine **begrenzte Angriffsfläche** haben, können einige von ihnen **Schwachstellen** oder **potenzielle Verbesserungen der Absicherung** enthalten. Die folgenden sind die häufigsten:
|
||||
Auch wenn Browser-Erweiterungen eine **begrenzte Angriffsfläche** haben, können einige von ihnen **Schwachstellen** oder **potenzielle Verbesserungen der Absicherung** enthalten. Die folgenden sind die häufigsten:
|
||||
|
||||
* [ ] **Beschränken** Sie so weit wie möglich die angeforderten **`Berechtigungen`**
|
||||
* [ ] **Beschränken** Sie so weit wie möglich die **`host_permissions`**
|
||||
|
@ -566,8 +629,8 @@ Obwohl Browser-Erweiterungen eine **begrenzte Angriffsfläche** haben, können e
|
|||
* **Fingerprint-Analyse**: Erkennung von [web\_accessible\_resources](https://developer.chrome.com/extensions/manifest/web\_accessible\_resources) und automatische Generierung von JavaScript zur Erstellung von Chrome-Erweiterungs-Fingerabdrücken.
|
||||
* **Potenzielle Clickjacking-Analyse**: Erkennung von Erweiterungs-HTML-Seiten mit der Direktive [web\_accessible\_resources](https://developer.chrome.com/extensions/manifest/web\_accessible\_resources). Diese sind je nach Zweck der Seiten potenziell anfällig für Clickjacking.
|
||||
* **Berechtigungswarnungs-Viewer**: zeigt eine Liste aller Chrome-Berechtigungswarnungen an, die angezeigt werden, wenn ein Benutzer versucht, die Erweiterung zu installieren.
|
||||
* **Gefährliche Funktionen**: zeigt den Ort gefährlicher Funktionen an, die potenziell von einem Angreifer ausgenutzt werden könnten (z. B. Funktionen wie innerHTML, chrome.tabs.executeScript).
|
||||
* **Einstiegspunkte**: zeigt, wo die Erweiterung Benutzer-/externe Eingaben entgegennimmt. Dies ist nützlich, um den Oberflächenbereich einer Erweiterung zu verstehen und nach potenziellen Punkten zu suchen, an die bösartig gestaltete Daten gesendet werden können.
|
||||
* **Gefährliche Funktionen**: zeigt den Ort gefährlicher Funktionen, die von einem Angreifer potenziell ausgenutzt werden könnten (z. B. Funktionen wie innerHTML, chrome.tabs.executeScript).
|
||||
* **Einstiegspunkte**: zeigt, wo die Erweiterung Benutzer-/externe Eingaben entgegennimmt. Dies ist nützlich, um den Umfang einer Erweiterung zu verstehen und nach potenziellen Punkten zu suchen, an die bösartig gestaltete Daten gesendet werden können.
|
||||
* Sowohl die Scanner für Gefährliche Funktionen als auch Einstiegspunkte haben folgendes für ihre generierten Warnungen:
|
||||
* Relevanten Code-Schnipsel und Zeile, die die Warnung verursacht hat.
|
||||
* Beschreibung des Problems.
|
||||
|
@ -575,11 +638,11 @@ Obwohl Browser-Erweiterungen eine **begrenzte Angriffsfläche** haben, können e
|
|||
* Der Pfad der alarmierten Datei.
|
||||
* Die vollständige Chrome-Erweiterungs-URI der alarmierten Datei.
|
||||
* Der Dateityp, z. B. ein Hintergrundseiten-Skript, ein Inhalts-Skript, eine Browseraktion usw.
|
||||
* Wenn die anfällige Zeile in einer JavaScript-Datei ist, die Pfade aller Seiten, auf denen sie enthalten ist, sowie der Status dieser Seiten als [web\_accessible\_resource](https://developer.chrome.com/extensions/manifest/web\_accessible\_resources).
|
||||
* **Content Security Policy (CSP)-Analysator und Bypass-Prüfer**: Dies zeigt Schwächen in der CSP Ihrer Erweiterung auf und beleuchtet auch mögliche Umgehungsmöglichkeiten Ihrer CSP aufgrund von whitelisteten CDNs usw.
|
||||
* Wenn die anfällige Zeile in einer JavaScript-Datei ist, die Pfade aller Seiten, auf denen sie enthalten ist, sowie den Status dieser Seiten als [web\_accessible\_resource](https://developer.chrome.com/extensions/manifest/web\_accessible\_resources).
|
||||
* **Content Security Policy (CSP)-Analyzer und Bypass-Checker**: Dies zeigt Schwächen in der CSP Ihrer Erweiterung auf und beleuchtet auch mögliche Umgehungsmöglichkeiten Ihrer CSP aufgrund von whitelisteten CDNs usw.
|
||||
* **Bekannte verwundbare Bibliotheken**: Verwendet [Retire.js](https://retirejs.github.io/retire.js), um auf die Verwendung von bekannten verwundbaren JavaScript-Bibliotheken zu prüfen.
|
||||
* Erweiterung und formatierte Versionen herunterladen.
|
||||
* Die originale Erweiterung herunterladen.
|
||||
* Die Originalerweiterung herunterladen.
|
||||
* Eine verschönerte Version der Erweiterung herunterladen (automatisch formatiertes HTML und JavaScript).
|
||||
* Automatisches Zwischenspeichern von Scanergebnissen, das Ausführen eines Erweiterungsscans dauert beim ersten Mal eine gute Zeit. Beim zweiten Mal, vorausgesetzt die Erweiterung wurde nicht aktualisiert, erfolgt die Ausführung fast sofort aufgrund der zwischengespeicherten Ergebnisse.
|
||||
* Verlinkbare Berichts-URLs, um jemand anderen leicht zu einem von Tarnish generierten Erweiterungsbericht zu verlinken.
|
||||
|
|
Loading…
Reference in a new issue