Trust GitHub Repositories by ID
Build a Node API, exploit a repo rename, and fix trust with repository IDs.
Introduction
30 Second Summary
A familiar name can feel like proof that the same person is still behind it. Once that name changes hands, a trusted label can quietly point to someone new.
In this project, you will reproduce a repository-name trust flaw in a local Node.js API. A numeric GitHub repository ID becomes the durable trust key that blocks the impostor.
What You'll Build
Your terminal will show the same impostor publish request receiving 200 from the name-based API and 403 from the ID-based API.
By the end of this project, you'll have:
- A vulnerable publish check where you can submit an allowed owner/repo name and see the impostor receive 200.
- A live repository lookup that shows a repository's name beside its numeric repository ID.
- A fixed authorization check that rejects the impostor with 403. The renamed original still receives 200 under its new name.
- An optional Secret Mission to push your authorization skills further.
What do I need before starting?
This project assumes Node.js plus basic comfort with curl. You also need a free GitHub account with a repository you can safely rename.
Before We Start
Your API needs a predictable local environment before you test its authorization decisions. A missing tool can make an environment failure look like a trust-check bug.
Node.js runs the local API. npm manages the project around it.
Your signed-in GitHub browser session gives you control of the test repositories. The curl client sends HTTP requests to the APIs you inspect.
In this step, get ready to:
- Confirm the local runtime is ready.
- Prepare access for live repository lookups.
- Create the empty repo-trust-check folder.
Prepare the local runtime
A terminal gives you one place to run the API and send test requests. Start there so you can verify both runtime commands.
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type Terminal on macOS or PowerShell on Windows into the search bar.
- Press Enter to open the selected terminal.
- Print the installed Node.js and npm versions by running these commands:
node --version
npm --version
What Do These Checks Prove?
- The node --version command confirms that your terminal can find the Node.js runtime.
- The npm --version command confirms that npm was installed alongside Node.js.
✔️ I see two version numbers
Good start. Your terminal can reach both Node.js and npm, so the local runtime is ready.
ⓧ A command is not found
The Node.js installer includes npm. Your operating system may ask for permission before the installation begins.
- Open the official Node.js download page in your browser.
- Select the installer labelled LTS for your operating system.
- Open the downloaded installer.
- Accept the operating system permission request if one appears.
- Keep the default installer choices.
- Close the terminal window after installation finishes.
- Return to your operating system search to start a fresh terminal session.
- Repeat the version check shown above.
Still Missing Node.js or npm?
A terminal opened before installation may still use its old command path. Close every terminal window before starting a fresh session.
If one command still cannot be found, help me diagnose why my terminal cannot find Node.js or npm.
Prepare live repository access
The attack simulation uses repositories you control. A signed-in browser session lets you rename the original repository during the test.
- Navigate to the official GitHub sign-in page in your browser.
- Choose the tab that matches what you see.
✔️ I see my GitHub home page
Your browser session is ready. You can use this account to control the repositories in the attack simulation.
ⓧ I see the sign-in form
- Enter your GitHub username or email address.
- Enter your GitHub password.
- Click Sign in.
- Complete any additional verification requested by GitHub.
You should reach your GitHub home page after authentication succeeds.
ⓧ I need a GitHub account
- Open the official GitHub account creation page.
- Follow the prompts to create your personal account.
- Verify your email address when GitHub asks.
- Return to the GitHub sign-in page.
- Enter your new account credentials.
- Click Sign in.
You should reach your GitHub home page with your new account signed in.
The curl command-line client sends requests to web APIs. Checking it now keeps the later GitHub and local API tests focused on their responses.
- Switch back to the terminal from earlier.
- Print the installed curl version by running this command:
curl --version
What Does This Check Prove?
The curl --version command confirms that the curl client is available from your terminal. Its output also identifies the installed build.
✔️ I see curl version details
Your HTTP client is ready. You can use it to inspect GitHub data and test the local API.
ⓧ The curl command is missing
Finding the right curl package can be fiddly because the download table covers many operating systems. Focus on the row for your platform.
- Open the official curl download page in your browser.
- Find the row for your operating system.
- Select a package that includes the curl tool.
- Follow the package provider's installation instructions.
- Switch back to your terminal.
- Repeat the curl version check shown above.
Still Cannot Run curl?
A newly installed command may need a fresh terminal session before it becomes available. Close the current terminal window before opening a new one through operating system search.
If the command remains unavailable, help me make curl available in my terminal.
Create the project folder
A dedicated folder keeps every version of the trust check together. It starts empty so the first API files have a clean home.
- Move to your Desktop and print its path by running these commands:
cd ~/Desktop
pwd
What Do These Commands Do?
- The cd command moves your terminal to the Desktop folder.
- The pwd command prints the folder your terminal is using.
The printed path should end with Desktop.
- Create the repo-trust-check folder and move into it by running these commands:
mkdir repo-trust-check
cd repo-trust-check
What Do These Commands Do?
- The mkdir command creates the local project folder on your Desktop.
- The cd command makes that new folder your terminal's current location.
Before you check, which folder name do you expect to see at the end of the printed path?
- Verify the folder path and its empty contents by running these commands:
pwd
ls
What Should You See?
- The pwd output should end with repo-trust-check.
- The ls command should return to the prompt without listing any file names.
Folder Path or Contents Look Different?
If the path does not end with repo-trust-check, repeat the Desktop and folder commands above in order.
Removing an existing folder can delete work you still need. Move any important files elsewhere before removing that folder through Finder or File Explorer.
If you are unsure what the terminal output means, help me verify that repo-trust-check is an empty folder on my Desktop.
That is the setup hurdle cleared. Next, you'll run the name-keyed API and watch a trusted repository receive a 200 response.
Run a Name-Based Trust Check
A publish gate needs a clear way to decide which repository can release a package. Your first gate uses the repository's owner and name as that identity.
In this step, you will build a local Node.js API that checks an allowlist. You will send a trusted repository name over HTTP and watch the API approve it.
In this step, get ready to:
- Configure the project manifest with a start command.
- Build an API that checks an owner and repository name against an allowlist.
- Send a trusted publish request and confirm that it receives HTTP 200.
Configure the project manifest
The package.json manifest records how npm starts your API. This project uses Node.js's built-in HTTP module, so it does not need an external package.
- Create package.json inside repo-trust-check by pasting the following manifest into a new file:
{
"name": "repo-trust-check",
"version": "1.0.0",
"private": true,
"description": "A local API for comparing repository trust checks.",
"scripts": {
"start": "node server-vulnerable.js"
},
"dependencies": {}
}
What does this manifest do?
- The private setting prevents npm from publishing this teaching project by accident.
- The start script tells npm to run server-vulnerable.js.
- The empty dependencies object confirms that the API only uses features built into Node.js.
- Save package.json.
- Switch back to the terminal from earlier.
- Confirm that npm can read the start script by running:
npm run
What does this command check?
Running npm run without a script name lists the scripts available in package.json. This confirms that the manifest is valid before the API depends on it.
You should see a start entry that points to node server-vulnerable.js.
Can't see the start script?
Confirm that package.json is directly inside repo-trust-check. Check the commas and double quotes if npm reports that it cannot read the manifest.
Help me fix my package.json start script.
✔️ Awesome, I've got everything!
Your manifest is saved with the command that starts the vulnerable API.
ⓧ I'd like to double check the full code
{
"name": "repo-trust-check",
"version": "1.0.0",
"private": true,
"description": "A local API for comparing repository trust checks.",
"scripts": {
"start": "node server-vulnerable.js"
},
"dependencies": {}
}
Build and start the name-based API
An allowlist contains the identities permitted to perform an action. This version stores one identity as an owner/repo string.
The API reads an incoming JSON request body. It joins the supplied owner and repo values before checking the resulting name.
- Enter your GitHub username here: YOUR_GITHUB_USERNAME.
- Create server-vulnerable.js inside repo-trust-check by pasting the following code into a new file:
const http = require('node:http');
const PORT = 3000;
const TRUSTED_REPOSITORIES = new Set([
'[[YOUR_GITHUB_USERNAME="YOUR_GITHUB_USERNAME"]]/trusted-publisher',
]);
const server = http.createServer((request, response) => {
// Collect the JSON body sent by the publish client.
let body = '';
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
// Turn the owner/repo pair into the allowlist key.
const { owner, repo } = JSON.parse(body);
const repositoryName = `${owner}/${repo}`;
const trusted = TRUSTED_REPOSITORIES.has(repositoryName);
response.statusCode = trusted ? 200 : 403;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify({ trusted, repository: repositoryName }));
});
});
server.listen(PORT, () => {
console.log(`Vulnerable API listening on http://localhost:${PORT}`);
});
What does this code do?
- TRUSTED_REPOSITORIES holds the repository names allowed to publish.
- The request listeners collect the incoming body before parsing its JSON fields.
- repositoryName combines the supplied owner and repository into the same format used by the allowlist.
- The response uses status 200 for a match. It uses status 403 for any other name.
- Save server-vulnerable.js.
The server keeps the terminal session busy while it listens for requests. Leave that session running during the next check.
- Start the vulnerable API from the terminal for repo-trust-check by running:
npm start
What does this command do?
npm reads the start script from package.json. Node.js then runs server-vulnerable.js and listens on port 3000.
You should see Vulnerable API listening on http://localhost:3000. That is the server online and ready to evaluate a repository name.
Server did not start?
Confirm that both files are inside repo-trust-check. Check that the filename in the start script exactly matches server-vulnerable.js.
Help me start my local Node.js API.
✔️ Awesome, I've got everything!
Your name-based API is running on port 3000.
ⓧ I'd like to double check the full code
const http = require('node:http');
const PORT = 3000;
const TRUSTED_REPOSITORIES = new Set([
'[[YOUR_GITHUB_USERNAME="YOUR_GITHUB_USERNAME"]]/trusted-publisher',
]);
const server = http.createServer((request, response) => {
// Collect the JSON body sent by the publish client.
let body = '';
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
// Turn the owner/repo pair into the allowlist key.
const { owner, repo } = JSON.parse(body);
const repositoryName = `${owner}/${repo}`;
const trusted = TRUSTED_REPOSITORIES.has(repositoryName);
response.statusCode = trusted ? 200 : 403;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify({ trusted, repository: repositoryName }));
});
});
server.listen(PORT, () => {
console.log(`Vulnerable API listening on http://localhost:${PORT}`);
});
Send a trusted publish request
The allowlist only succeeds when the request produces the same owner and repository string. This check gives you the accepted baseline that later trust checks can be compared against.
- Use your terminal's new-tab control to open a second terminal session.
Before you send the request, do you expect the supplied repository name to match the allowlist?
- Send the trusted publish request to the running API by running:
curl -s -w '\nHTTP %{http_code}\n' \
-H 'Content-Type: application/json' \
--data '{"owner":"[[YOUR_GITHUB_USERNAME="YOUR_GITHUB_USERNAME"]]","repo":"trusted-publisher"}' \
http://localhost:3000
What does this request do?
- The request sends your username as the owner value.
- The request sends trusted-publisher as the repo value.
- The Content-Type: application/json header tells the API how to interpret the request body.
- The write-out option prints the final HTTP status beneath the response body.
You should see "trusted":true in the response body. The final line should show HTTP 200.
You have the baseline working: the name on the request matches the name on the allowlist, so the API grants publish access.
Request did not return HTTP 200?
Keep the first terminal running with the API. Confirm that the username in the request matches the username stored in TRUSTED_REPOSITORIES exactly.
Help me debug my local publish request.
Your API now approves a repository by its name. Next, you will look up a live GitHub repository and place its numeric ID beside that familiar name.
Inspect the Repository ID
Your local API now accepts a publish request when its owner and repository name match the allowlist. That confirms the name-based check behaves exactly as written.
A repository name can change owners over time. In this step, you’ll compare the name shown by GitHub with the numeric repository ID that stays attached to the repository.
In this step, get ready to:
- Create the public trusted-publisher test repository in your GitHub account.
- Fetch the repository details from GitHub.
- Record the top-level repository ID for later trust checks.
Create the test repository
The public lookup needs a real repository with the same name your vulnerable API already trusts. You’ll create that repository under your personal GitHub account.
- In your signed-in GitHub browser tab, open the new repository page.
- Confirm the Owner dropdown shows your personal account.
You’ll see your username under Owner. This confirms the repository belongs to your personal account.
- Record the selected username here: YOUR_GITHUB_USERNAME.
- Enter trusted-publisher in the Repository name field.
The form now shows trusted-publisher as the repository name.
- Select Public under Choose visibility.
- Click Create repository.
You’ll land on the repository page for YOUR_GITHUB_USERNAME/trusted-publisher. Good work. Your public test repository now gives the trust check a real GitHub identity to inspect.
Can't Create the Repository?
- Check your account repositories for an existing trusted-publisher repository.
- Use that existing repository only if it is your intended public test repository.
Ask for help to resolve the repository-name conflict safely.
Fetch and record the repository ID
GitHub’s public REST API returns repository details as JSON. The response includes the repository name plus several numeric identifiers.
- Press Cmd+T on macOS or Ctrl+Shift+T on Windows in your terminal to create a new tab.
- Prepare the official repository lookup by pasting this template into the new terminal tab without pressing Enter:
curl -L \
-X GET \
https://api.github.com/repos/OWNER/REPO
What Does This Request Do?
- The curl request asks GitHub for one repository using its OWNER and REPO path values.
- Public repository details are available without authentication.
- Replace OWNER with YOUR_GITHUB_USERNAME.
- Replace REPO with trusted-publisher.
Before you send this request, do you expect GitHub to identify the repository only by its name? Hold that prediction for the response.
- Press Enter to send the request.
You’ll see a JSON response. Near the top, full_name displays YOUR_GITHUB_USERNAME/trusted-publisher.
The first top-level id displays a number. You have captured the identity snapshot needed for the attack test.
Repository Details Missing?
- Check that OWNER matches your GitHub username exactly.
- Check that REPO is set to trusted-publisher.
- Confirm the repository visibility is Public.
Ask for help to diagnose the repository lookup response.
Which ID Should You Keep?
The first top-level id identifies the repository. An id nested inside owner identifies the account.
The full_name field provides the readable owner and repository name. The top-level id provides the stable trust key.
- Record the number beside the first top-level id here: ORIGINAL_REPOSITORY_ID.
You now have the original repository’s stable identity recorded. Next, you’ll see whether a different repository can inherit trust by claiming the same name.
Recreate the Trusted Name
Your local API treats the owner/repo string as the identity behind a publish request. That choice ties authorization to a name in the allowlist.
You will move the original repository to a new name on GitHub. You will create a different repository under the trusted name. The local check stays unchanged. The result reveals which repository it trusts.
In this step, get ready to:
- Rename the original repository to trusted-publisher-renamed.
- Create an impostor repository at the old trusted-publisher name.
- Send the impostor name to the vulnerable API.
Rename the original repository
A repository path can change. Its numeric repository ID continues to identify the original resource.
- Return to the main page for YOUR_GITHUB_USERNAME/trusted-publisher in your signed-in browser.
- Click Settings under the repository name.
You will see the current name in the Repository Name field.
- Replace the value in Repository Name with trusted-publisher-renamed.
- Click Rename.
That rename is complete. The repository header now shows YOUR_GITHUB_USERNAME/trusted-publisher-renamed.
- Return to the HTTP client you used for the public repository lookup.
- Replace the repository name at the end of the previous lookup with trusted-publisher-renamed.
Before you send the lookup, do you expect the numeric repository ID to change with the name?
- Send the updated repository lookup.
The top-level id should match your original repository ID. The full_name should now show YOUR_GITHUB_USERNAME/trusted-publisher-renamed.
Seeing a Different Repository ID?
- Check the top-level id in the response.
- Confirm the lookup ends with trusted-publisher-renamed.
Ask for help comparing the repository fields.
Create the impostor repository
The impostor only needs the old repository path. Its contents do not affect the vulnerable name check.
- Select the menu in the upper-right corner of GitHub.
- Click New repository.
You will see the form for creating a repository under your account.
- Use the Owner dropdown to select your personal account.
- Enter trusted-publisher in the Repository name field.
The public GitHub API must be able to read this test repository without credentials.
The repository contains no personal code, so this visibility choice only exposes the empty test resource.
- Select Public.
- Click Create repository.
The takeover setup is now in place. You have a different repository at YOUR_GITHUB_USERNAME/trusted-publisher.
- Return to the HTTP client from the repository lookup.
- Replace the ending trusted-publisher-renamed with trusted-publisher.
Before you send this lookup, do you expect the old repository ID or a new repository ID?
- Send the lookup for the recreated repository name.
The response shows YOUR_GITHUB_USERNAME/trusted-publisher in full_name. The top-level id now belongs to the impostor.
- Record the impostor's numeric id here: your impostor repository ID.
- Compare your impostor repository ID with your original repository ID.
The values differ. That proves the old name now points to a different repository.
Unable to Claim the Old Name?
- Confirm the original repository header shows trusted-publisher-renamed.
- Confirm the new repository owner is your personal account.
- Retry trusted-publisher in the Repository name field.
Ask for help reclaiming the old repository name.
Test the vulnerable API
The vulnerable API compares only the submitted owner and repository name with its allowlist. Both repositories have occupied the same trusted path at different times.
Before you resend the publish request, do you think the vulnerable API can tell that this name now belongs to a different repository?
- Switch back to the terminal where you sent the earlier publish request.
- Press the Up Arrow until the previous publish request appears.
- Confirm owner still contains YOUR_GITHUB_USERNAME.
- Confirm repo still contains trusted-publisher.
- Press Enter to send the request.
There it is. You will see HTTP 200 even though the trusted name now belongs to the impostor repository.
What Does This Prove?
- The allowlist recognizes the string YOUR_GITHUB_USERNAME/trusted-publisher.
- The impostor now owns that exact repository path.
- The vulnerable API never checks the repository's numeric id.
Not Seeing HTTP 200?
- Confirm the local API is still running on http://localhost:3000.
- Confirm the running file is server-vulnerable.js.
- Confirm the request uses the original repository name trusted-publisher.
Ask for help tracing the rejected request.
You have reproduced the trust failure with two repositories that share one name across time. Next, you will make the authorization decision follow the repository itself.
Trust the Immutable Repository ID
The impostor repository just crossed your publish boundary because its new name matched the allowlist. Your API had no stable identity to tell it apart from the original.
The fix anchors authorization to an immutable repository ID. On every request, the GitHub REST API resolves the submitted name to its current numeric ID before your API decides.
Why Trust the Repository ID?
A repository name can be reassigned after a rename. Its numeric ID remains tied to the original repository.
The ID becomes the authorization anchor. The name becomes lookup input.
In this step, get ready to:
- Store the original repository's numeric ID in a fixed allowlist.
- Resolve each submitted repository through the public GitHub API.
- Reject the impostor repository with HTTP 403.
Create the ID-based trust check
GitHub's GET /repos/{owner}/{repo} endpoint returns the repository currently using a submitted name. The fixed API uses the returned id as the trust decision.
- Switch back to the code editor you used for the vulnerable API.
- Create server-fixed.js inside the repo-trust-check folder.
- Add the fixed server by pasting this code into server-fixed.js. Use your original numeric ID for ORIGINAL_REPOSITORY_ID:
const express = require('express');
const app = express();
const port = 3000;
// Keep authorization anchored to the original repository.
const trustedRepositoryIds = new Set([Number('[[ORIGINAL_REPOSITORY_ID="ORIGINAL_REPOSITORY_ID"]]')]);
app.use(express.json());
app.post('/publish', async (req, res) => {
// Resolve the submitted name to the repository using it now.
const { owner, repo } = req.body;
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
if (!response.ok) {
return res.status(502).json({ error: 'GitHub lookup failed' });
}
const repository = await response.json();
// Authorize only the immutable identity returned by GitHub.
const allowed = trustedRepositoryIds.has(repository.id);
return res.status(allowed ? 200 : 403).json({
allowed,
repositoryId: repository.id
});
});
app.listen(port, () => {
console.log(`Fixed API listening on http://localhost:${port}`);
});
What Does This Code Do?
- The trustedRepositoryIds set stores the original repository's numeric identity.
- The fetch() call resolves the submitted owner and repository name through GitHub.
- The allowed value becomes true only when the returned repository.id exists in the trusted set.
- A matching ID receives HTTP 200. Every other repository ID receives HTTP 403.
- Save server-fixed.js.
- Return to the terminal running server-vulnerable.js.
- Stop the vulnerable server by pressing Ctrl+C.
The fixed server keeps this terminal occupied while it listens for requests. Leave it running during the final check.
- Start the fixed API from the repo-trust-check folder by running this command:
node server-fixed.js
What Does This Command Do?
The node command runs server-fixed.js. The script starts the ID-based API on http://localhost:3000.
You'll see Fixed API listening on http://localhost:3000 in the terminal. Your ID-based trust check is now accepting requests.
Server Does Not Start?
If the port is already in use, return to the vulnerable server terminal. Press Ctrl+C before running the fixed server again.
If the server exits immediately, confirm that server-fixed.js is inside repo-trust-check. Compare the file with the full-code reference below.
Help me diagnose why server-fixed.js does not start.
✔️ Awesome, I've got everything!
Your vulnerable example remains in server-vulnerable.js. Your fixed implementation is saved in server-fixed.js.
ⓧ I'd like to double check the full code
const express = require('express');
const app = express();
const port = 3000;
// Keep authorization anchored to the original repository.
const trustedRepositoryIds = new Set([Number('[[ORIGINAL_REPOSITORY_ID="ORIGINAL_REPOSITORY_ID"]]')]);
app.use(express.json());
app.post('/publish', async (req, res) => {
// Resolve the submitted name to the repository using it now.
const { owner, repo } = req.body;
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
if (!response.ok) {
return res.status(502).json({ error: 'GitHub lookup failed' });
}
const repository = await response.json();
// Authorize only the immutable identity returned by GitHub.
const allowed = trustedRepositoryIds.has(repository.id);
return res.status(allowed ? 200 : 403).json({
allowed,
repositoryId: repository.id
});
});
app.listen(port, () => {
console.log(`Fixed API listening on http://localhost:${port}`);
});
Retest the impostor repository
The request still names YOUR_GITHUB_USERNAME/trusted-publisher. The fixed API now performs a live identity lookup before deciding.
- Return to the HTTP client you used for the impostor request earlier.
- Reuse the exact publish request that previously received HTTP 200.
Before you send it again, do you think the repository name can still override the numeric identity check?
- Send the impostor publish request again.
You'll see HTTP 403 with allowed set to false.
The repositoryId value is IMPOSTOR_REPOSITORY_ID. It differs from trusted ID ORIGINAL_REPOSITORY_ID.
You closed the hole: the same impostor request that passed by name now fails by identity.
Still Seeing HTTP 200?
Confirm the server terminal says Fixed API listening on http://localhost:3000. A message from the vulnerable server means the old file is still running.
Check that trustedRepositoryIds contains the original repository ID. The impostor ID must remain outside the set.
Help me find why the impostor still receives HTTP 200.
Your authorization boundary now follows the original repository's identity. Next, you'll confirm that the rightful repository keeps its access after the rename.
Verify the Renamed Original
Your fixed API has already rejected the impostor because GitHub returned a different numeric repository ID.
The stronger rule also needs to preserve access for the real repository after a rename. You will send the renamed original through the same check to prove its immutable ID still earns permission.
In this step, get ready to:
- Send a publish request for the renamed original through the fixed API.
- Compare its HTTP 200 result with the impostor's HTTP 403 result.
Send the renamed repository request
The original repository now uses the name trusted-publisher-renamed. Its numeric identity remains ORIGINAL_REPOSITORY_ID.
- Switch back to the terminal panel where you sent the fixed API request for the impostor.
- Press the Up Arrow key until that request appears.
- Replace only trusted-publisher with trusted-publisher-renamed in the request body.
Before you send the request, pause to consider whether the renamed repository resolves to its trusted ID or a new ID.
- Press Enter to send the edited request.
You should see an HTTP 200 response indicating that publishing is allowed.
That closes the second half of the test. The renamed original still publishes because its trusted identity survived the name change.
Why Does the Renamed Original Pass?
The fixed API asks GitHub for live repository data using the new name. GitHub returns ORIGINAL_REPOSITORY_ID for the same underlying repository.
The allowlist already trusts that ID. The repository name can change without transferring permission.
Still Seeing HTTP 403?
- Check that the request body uses trusted-publisher-renamed.
- Confirm that the fixed API process from earlier is still running on http://localhost:3000.
- Get help with the renamed repository request.
Compare both authorization decisions
The visible repository names changed ownership during the test. The numeric IDs reveal which repository receives permission.
Before you compare the responses, pause to consider whether the repository name or numeric ID lines up with each decision.
- Scroll to the earlier fixed API response for the impostor repository.
- Compare its HTTP 403 result with the renamed original's HTTP 200 result.
The impostor YOUR_GITHUB_USERNAME/trusted-publisher remains rejected with HTTP 403 because it resolves to IMPOSTOR_REPOSITORY_ID.
The renamed original YOUR_GITHUB_USERNAME/trusted-publisher-renamed is accepted with HTTP 200 because it resolves to trusted ID ORIGINAL_REPOSITORY_ID.
You have now proved both sides of the fix. Permission follows stable identity through a rename.
Your ID-based rule now handles the complete rename scenario. Next, you will audit your own code for trust checks built on changeable names.
Document Other Renameable Trust Checks
Your GitHub test proved that a numeric repository ID survives a rename. The fixed authorization check rejected the impostor even though it claimed the trusted name.
Production systems often make trust decisions using email addresses, slugs, usernames, or paths. In this step, you will turn your demo into an audit that you can apply to your own code.
In this step, get ready to:
- Document the name-reuse attack and immutable-ID fix.
- Audit your code for trust checks based on renameable identifiers.
- Record candidates that should use immutable IDs.
Create the audit record
A security lesson becomes reusable when the repository explains the failure and the fix. Your README.md will preserve that explanation beside both API versions.
What belongs in the audit?
Each entry identifies the code location and the protected decision. It also names the mutable value that currently controls trust.
The final note proposes an immutable identifier that stays attached to the same account or resource after a rename.
- In the repo-trust-check folder from earlier, use your terminal editor to create README.md with headings named Name-Reuse Attack, Immutable-ID Fix, and Renameable Trust Check Audit.
- Save README.md.
- Read README.md from top to bottom in your terminal editor.
You should see all three headings in the new file. The audit section now has a clear home beside the attack explanation.
Can't find the new README?
- Check that your terminal editor shows the repo-trust-check folder path.
- Return to the repo-trust-check folder from earlier before creating the file again.
Ask for help with locating README.md in the project folder.
Capture the demonstrated weakness
Your two API versions provide a complete audit example. One trusts a renameable repository name while the other resolves the live repository ID before deciding whether to publish.
Facts to capture
- The vulnerable check in server-vulnerable.js grants publish permission when the submitted owner and repository name matches YOUR_GITHUB_USERNAME/trusted-publisher.
- The impostor claimed that old repository name and received HTTP 200 from the vulnerable API.
- The fixed check in server-fixed.js compares the live numeric id with ORIGINAL_REPOSITORY_ID.
- The impostor resolved to IMPOSTOR_REPOSITORY_ID and received HTTP 403 from the fixed API.
- The renamed original kept ORIGINAL_REPOSITORY_ID and received HTTP 200 under YOUR_GITHUB_USERNAME/trusted-publisher-renamed.
- Fill the three sections in README.md using the facts above.
- Save README.md.
- Read the completed attack explanation and first audit entry.
You should see how reusing the trusted name lets the impostor pass the vulnerable check. You should also see how the numeric ID blocks that same repository.
That is the central lesson captured: the repository name can move while the repository ID stays attached to the original resource.
Audit your own trust checks
A trust check deserves review whenever a mutable field decides who can publish, administer, approve, or own a resource. The risky pattern can appear outside repository allowlists.
Signals worth searching for
- Names or usernames used to grant a privileged action.
- Email addresses used as permanent account identities.
- Slugs or handles used as authorization keys.
- Paths or display labels used to identify protected resources.
- Review every source file in repo-trust-check for comparisons where a mutable identifier controls a trust decision.
- Repeat the review in one other app you control if one is available.
- Add each finding to the Renameable Trust Check Audit section with its location, protected decision, mutable key, and immutable-ID candidate.
- Save README.md.
- Predict which mutable identifier will appear most often before you inspect the finished audit.
- Read README.md from top to bottom.
- Confirm that every audit entry names the code location and protected decision.
- Confirm that every audit entry identifies the mutable key and a possible immutable replacement.
- Confirm that the project summary explains why the impostor passed the name check but failed the ID check.
You should see the name-reuse attack, the immutable-ID fix, and your audit list in one document. Each finding should point toward a stable identity that can replace a renameable trust key.
Strong finish. Your repository now demonstrates the vulnerability and gives you a practical checklist for finding the same pattern elsewhere.
Secret mission
Design an Authorization Invariant
Turn one mutable trust check from your README audit into a reusable authorization invariant. Define the stable identity behind the check. Prove that the rule rejects the impostor while preserving access for the renamed original.
Clean Up Your Resources
Clean Up Your Resources
No paid cloud resources are running, so this project has no ongoing service costs. Choose whether to keep the local demo, pause its API process, or remove the temporary resources.
Resources you used:
- The running Node.js API process serving http://localhost:3000.
- The local repo-trust-check folder containing the vulnerable API, fixed API, project manifest, and audit notes.
- The impostor GitHub repository at YOUR_GITHUB_USERNAME/trusted-publisher.
- The temporary rename applied to the original repository at YOUR_GITHUB_USERNAME/trusted-publisher-renamed.
Keep everything running
No action is needed. Choose this option if you want to repeat the attack demonstration or continue auditing trust checks.
- Keep the API process running while you continue testing requests against http://localhost:3000.
- Keep the repo-trust-check folder as a before and after example of mutable-name trust compared with immutable-ID trust.
- Keep both test repositories in their current state so you can demonstrate the name-reuse attack again.
Pause - I'll come back to this later
Shut down the local API process to free its port. Your project files and test repositories remain available for later.
- Return to the terminal panel running the API.
- Press Ctrl+C to stop the server.
- Confirm that the terminal prompt returns.
- Keep the repo-trust-check folder with its README.md audit notes.
- Leave both GitHub test repositories unchanged until you resume the demonstration.
Delete - I don't want to use this again
Remove the project resources if you want to start fresh. Deletion is permanent, so copy README.md first if you want to keep your audit notes.
- Return to the terminal panel running the API.
- Press Ctrl+C if the server is still running.
- Confirm that the terminal prompt returns.
The impostor repository exists only to demonstrate the attack. Remove it before restoring the original repository's old name.
- In your signed-in GitHub browser session, open the repository settings page for YOUR_GITHUB_USERNAME/trusted-publisher.
- Go to the repository deletion section.
- Enter YOUR_GITHUB_USERNAME/trusted-publisher when GitHub asks you to confirm the repository name.
- Complete the repository deletion.
- Open the repository settings page for YOUR_GITHUB_USERNAME/trusted-publisher-renamed.
- Change the repository name from trusted-publisher-renamed back to trusted-publisher.
- Confirm that the original repository page now uses YOUR_GITHUB_USERNAME/trusted-publisher.
The local project folder can be removed through your operating system's file manager. Choose the instructions for your computer.
macOS
- Press Cmd+Space to open the search bar.
- Type Finder.
- Press Enter to open Finder.
- Search Finder for repo-trust-check.
- Select the project folder containing package.json.
- Press Cmd+Delete to move the folder to the Bin.
- Empty the Bin to remove the folder permanently.
- Search for repo-trust-check again to confirm that the folder is gone.
Windows
Windows asks you to confirm permanent deletion when you use Shift+Delete.
- Press the Windows key to open the search bar.
- Type File Explorer.
- Press Enter to open File Explorer.
- Search File Explorer for repo-trust-check.
- Select the project folder containing package.json.
- Press Shift+Delete to start permanent deletion.
- Confirm the deletion when Windows prompts you.
- Search for repo-trust-check again to confirm that the folder is gone.
Nice Work!
Nice Work!
Well done! You used a local Node.js API to reproduce a repository name takeover against a GitHub-based trust check.
Your project now preserves both the vulnerable version and the fixed version. It proves that immutable repository IDs survive renames while reusable names can point to a new repository.
You learned how to:
- Build a name-based publish check that accepts an allowlisted repository. You proved its weakness when the impostor repository received HTTP 200.
- Replace mutable-name trust with an immutable repository ID. You proved the fix when the impostor received HTTP 403 while the renamed original still received HTTP 200.
- Audit your own authorization checks for names, email addresses, slugs, or other mutable identifiers. You documented the checks that should use immutable IDs in repo-trust-check/README.md.
- Secret Mission: Complete an optional challenge to push your immutable-identity checks further.
Ready to quiz yourself?