Skip to content

Commit 1b53bea

Browse files
committed
feat: add challenge 76 with simulated RAG vector store secret leak
1 parent 5d9ffc5 commit 1b53bea

24 files changed

Lines changed: 828 additions & 0 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ Now you can try to find the secrets by means of solving the challenge offered at
232232
- [localhost:8080/challenge/challenge-71](http://localhost:8080/challenge/challenge-71)
233233
- [localhost:8080/challenge/challenge-72](http://localhost:8080/challenge/challenge-72)
234234
- [localhost:8080/challenge/challenge-73](http://localhost:8080/challenge/challenge-73)
235+
- [localhost:8080/challenge/challenge-76](http://localhost:8080/challenge/challenge-76)
235236
</details>
236237

237238
Note that these challenges are still very basic, and so are their explanations. Feel free to file a PR to make them look
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.owasp.wrongsecrets.challenges.docker;
2+
3+
import java.util.List;
4+
import java.util.Locale;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.owasp.wrongsecrets.challenges.FixedAnswerChallenge;
7+
import org.springframework.stereotype.Component;
8+
9+
/**
10+
* Challenge about a retrieval augmented generation (RAG) vector store that indexed development
11+
* documents containing a synthetic secret. The simulated vector store exposes its indexed text
12+
* chunks through an unauthenticated search endpoint, exactly like a misconfigured vector database
13+
* that is reachable without authentication.
14+
*/
15+
@Slf4j
16+
@Component
17+
public class Challenge76 extends FixedAnswerChallenge {
18+
19+
static final String DEVELOPMENT_SECRET = "vector-store-dev-secret-e5c91a7b";
20+
21+
private static final List<IndexedChunk> INDEXED_CHUNKS =
22+
List.of(
23+
new IndexedChunk(
24+
"chunk-0",
25+
"Onboarding guide: the staging environment is reset every night at 02:00 UTC."),
26+
new IndexedChunk(
27+
"chunk-1",
28+
"Runbook: restart the ingestion worker with `kubectl rollout restart"
29+
+ " deployment/ingestion`."),
30+
new IndexedChunk(
31+
"chunk-2",
32+
"Draft API note: the development vector store uses the credential "
33+
+ DEVELOPMENT_SECRET
34+
+ " for the nightly embedding job. Rotate before production."),
35+
new IndexedChunk(
36+
"chunk-3", "Meeting notes: evaluation set v3 improved recall from 0.71 to 0.78."));
37+
38+
/** A single text chunk as it is stored in the simulated vector store. */
39+
public record IndexedChunk(String id, String text) {}
40+
41+
@Override
42+
public String getAnswer() {
43+
return DEVELOPMENT_SECRET;
44+
}
45+
46+
/**
47+
* Simulates a similarity search against the in-app vector store: every indexed chunk whose text
48+
* contains the query (case-insensitive) is returned, including the chunk that leaks the
49+
* development secret.
50+
*/
51+
public List<IndexedChunk> search(String query) {
52+
if (query == null || query.isBlank()) {
53+
return INDEXED_CHUNKS;
54+
}
55+
var normalizedQuery = query.toLowerCase(Locale.ROOT);
56+
return INDEXED_CHUNKS.stream()
57+
.filter(chunk -> chunk.text().toLowerCase(Locale.ROOT).contains(normalizedQuery))
58+
.toList();
59+
}
60+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.owasp.wrongsecrets.challenges.docker;
2+
3+
import java.util.List;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
/** REST controller for Challenge 76 exposing the simulated RAG vector store search. */
11+
@Slf4j
12+
@RestController
13+
@RequiredArgsConstructor
14+
public class Challenge76Controller {
15+
16+
private final Challenge76 challenge;
17+
18+
/**
19+
* Unauthenticated search endpoint of the simulated vector store. It returns the indexed text
20+
* chunks that match the query, leaking the development secret that was indexed along with the
21+
* other documents.
22+
*/
23+
@GetMapping("/rag/search")
24+
public List<Challenge76.IndexedChunk> search(
25+
@RequestParam(value = "q", required = false) String query) {
26+
log.info("Searching the simulated vector store for Challenge 76...");
27+
return challenge.search(query);
28+
}
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
=== Challenge 76: Find the Secret Indexed in the RAG Vector Store
2+
3+
Retrieval augmented generation (RAG) pipelines answer questions by first looking
4+
up relevant passages in a vector store and then feeding those passages to a large
5+
language model. To build that store, documents are split into chunks, turned into
6+
embeddings and indexed. The chunks themselves stay readable: a search endpoint
7+
returns the original text of every chunk it considers relevant.
8+
9+
That is exactly what went wrong in this challenge. A development team indexed its
10+
runbooks, meeting notes and draft API documentation into a vector store that is
11+
reachable from the application without any authentication:
12+
13+
`GET /rag/search`
14+
15+
The endpoint takes an optional query parameter `q` and returns the indexed text
16+
chunks that contain it. Without a query it returns every indexed chunk. One of the
17+
chunks was indexed while it still contained a draft note with the credential of
18+
the nightly embedding job.
19+
20+
Browse the indexed chunks through the endpoint and submit the credential you find
21+
in them.
22+
23+
[NOTE]
24+
====
25+
The vector store, the endpoint and every credential in this challenge are
26+
simulated and fictional. Nothing is sent over the network: the application only
27+
searches its own in-memory index.
28+
====
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
=== Challenge 76: Finde das Geheimnis im RAG-Vektorstore
2+
3+
Retrieval-Augmented-Generation-(RAG)-Pipelines beantworten Fragen, indem sie zuerst
4+
relevante Passagen in einem Vektorstore nachschlagen und diese Passagen dann an ein
5+
großes Sprachmodell übergeben. Um diesen Store aufzubauen, werden Dokumente in
6+
Chunks zerlegt, in Embeddings umgewandelt und indiziert. Die Chunks selbst bleiben
7+
lesbar: Ein Suchendpoint gibt den Originaltext jedes Chunks zurück, der als relevant
8+
gilt.
9+
10+
Genau das ist in dieser Challenge schiefgegangen. Ein Entwicklungsteam hat seine
11+
Runbooks, Meeting-Notizen und API-Entwurfsdokumentation in einem Vektorstore
12+
indiziert, der aus der Anwendung heraus ohne jegliche Authentifizierung erreichbar
13+
ist:
14+
15+
`GET /rag/search`
16+
17+
Der Endpoint akzeptiert einen optionalen Query-Parameter `q` und gibt die
18+
indizierten Textchunks zurück, die ihn enthalten. Ohne Query gibt er jeden
19+
indizierten Chunk zurück. Einer der Chunks wurde indiziert, als er noch eine
20+
Entwurfsnotiz mit dem Zugangscredential des nächtlichen Embedding-Jobs enthielt.
21+
22+
Durchsuche die indizierten Chunks über den Endpoint und reiche das darin gefundene
23+
Zugangscredential ein.
24+
25+
[NOTE]
26+
====
27+
Der Vektorstore, der Endpoint und jedes Zugangscredential in dieser Challenge sind
28+
simuliert und fiktiv. Nichts wird über das Netzwerk gesendet: Die Anwendung
29+
durchsucht nur ihren eigenen Index im Speicher.
30+
====
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
=== Desafío 76: Encuentra el secreto indexado en el almacén vectorial RAG
2+
3+
Los pipelines de generación aumentada por recuperación (RAG) responden preguntas
4+
buscando primero los pasajes relevantes en un almacén vectorial y alimentando
5+
después esos pasajes a un gran modelo de lenguaje. Para construir ese almacén, los
6+
documentos se dividen en fragmentos, se convierten en embeddings y se indexan. Los
7+
fragmentos en sí siguen siendo legibles: un endpoint de búsqueda devuelve el texto
8+
original de cada fragmento que considera relevante.
9+
10+
Exactamente eso es lo que salió mal en este desafío. Un equipo de desarrollo indexó
11+
sus runbooks, notas de reuniones y documentación de API en borrador en un almacén
12+
vectorial que es accesible desde la aplicación sin ninguna autenticación:
13+
14+
`GET /rag/search`
15+
16+
El endpoint acepta un parámetro de consulta opcional `q` y devuelve los fragmentos
17+
de texto indexados que lo contienen. Sin una consulta devuelve todos los fragmentos
18+
indexados. Uno de los fragmentos fue indexado mientras aún contenía una nota en
19+
borrador con la credencial del trabajo nocturno de embeddings.
20+
21+
Recorre los fragmentos indexados a través del endpoint y envía la credencial que
22+
encuentres en ellos.
23+
24+
[NOTE]
25+
====
26+
El almacén vectorial, el endpoint y todas las credenciales de este desafío son
27+
simulados y ficticios. No se envía nada por la red: la aplicación solo busca en su
28+
propio índice en memoria.
29+
====
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
=== Challenge 76 : Trouvez le secret indexé dans le magasin vectoriel RAG
2+
3+
Les pipelines de génération augmentée par récupération (RAG) répondent aux
4+
questions en recherchant d'abord les passages pertinents dans un magasin vectoriel,
5+
puis en alimentant ces passages avec un grand modèle de langage. Pour construire ce
6+
magasin, les documents sont découpés en fragments, convertis en embeddings et
7+
indexés. Les fragments eux-mêmes restent lisibles : un point d'accès de recherche
8+
renvoie le texte original de chaque fragment jugé pertinent.
9+
10+
C'est exactement ce qui s'est mal passé dans ce challenge. Une équipe de
11+
développement a indexé ses runbooks, comptes rendus de réunion et documentation API
12+
de brouillon dans un magasin vectoriel accessible depuis l'application sans aucune
13+
authentification :
14+
15+
`GET /rag/search`
16+
17+
Le point d'accès accepte un paramètre de requête optionnel `q` et renvoie les
18+
fragments de texte indexés qui le contiennent. Sans requête, il renvoie tous les
19+
fragments indexés. L'un des fragments a été indexé alors qu'il contenait encore une
20+
note de brouillon avec l'identifiant du travail d'embedding nocturne.
21+
22+
Parcourez les fragments indexés via le point d'accès et soumettez l'identifiant que
23+
vous y trouvez.
24+
25+
[NOTE]
26+
====
27+
Le magasin vectoriel, le point d'accès et tous les identifiants de ce challenge
28+
sont simulés et fictifs. Rien n'est envoyé sur le réseau : l'application ne fait
29+
que chercher dans son propre index en mémoire.
30+
====
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
You can solve this challenge using the following steps:
2+
3+
1. Open the unauthenticated search endpoint of the simulated vector store:
4+
- `http://localhost:8080/rag/search`
5+
- It returns all four indexed text chunks as JSON.
6+
7+
2. Read the chunks and look for the one that mentions a credential:
8+
- The chunk with the "Draft API note" contains the credential of the nightly
9+
embedding job.
10+
11+
3. Submit that credential (everything after "uses the credential ") as the answer.
12+
13+
You can also narrow the search, for example with `q=credential`, but the secret is
14+
only exposed because the chunk was indexed before the draft note was scrubbed.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Du kannst diese Challenge mit den folgenden Schritten lösen:
2+
3+
1. Öffne den unauthentifizierten Suchendpoint des simulierten Vektorstores:
4+
- `http://localhost:8080/rag/search`
5+
- Er gibt alle vier indizierten Textchunks als JSON zurück.
6+
7+
2. Lies die Chunks und suche nach demjenigen, der ein Zugangscredential erwähnt:
8+
- Der Chunk mit der "Draft API note" enthält das Zugangscredential des nächtlichen
9+
Embedding-Jobs.
10+
11+
3. Reiche dieses Zugangscredential als Antwort ein (alles nach
12+
"uses the credential ").
13+
14+
Du kannst die Suche auch eingrenzen, zum Beispiel mit `q=credential`, aber das
15+
Geheimnis liegt nur offen, weil der Chunk indiziert wurde, bevor die Entwurfsnotiz
16+
bereinigt wurde.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Puedes resolver este desafío con los siguientes pasos:
2+
3+
1. Abre el endpoint de búsqueda sin autenticación del almacén vectorial simulado:
4+
- `http://localhost:8080/rag/search`
5+
- Devuelve los cuatro fragmentos de texto indexados como JSON.
6+
7+
2. Lee los fragmentos y busca el que menciona una credencial:
8+
- El fragmento con la "Draft API note" contiene la credencial del trabajo nocturno
9+
de embeddings.
10+
11+
3. Envía esa credencial como respuesta (todo lo que hay después de
12+
"uses the credential ").
13+
14+
También puedes acotar la búsqueda, por ejemplo con `q=credential`, pero el secreto
15+
solo queda expuesto porque el fragmento fue indexado antes de que la nota en
16+
borrador se limpiara.

0 commit comments

Comments
 (0)