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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,9 @@ OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_SCOPE=
OIDC_EMAIL_CLAIM=

# Optional - Whitelist of allowed target domains for URL shortening
# Comma-separated list of domains. Use *.domain.com to allow domain and all subdomains.
# If empty, all domains are allowed.
# Example: *.github.qkg1.top,*.google.com,example.com
ALLOWED_TARGET_DOMAINS=
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ You can use files for each of the variables by appending `_FILE` to the name of
| `OIDC_SCOPE` | OIDC Scope | `openid profile email` | `openid email` |
| `OIDC_EMAIL_CLAIM` | Name of the field to get user's email from | `email` | `userEmail` |
| `REPORT_EMAIL` | The email address that will receive submitted reports | - | `example@yoursite.com` |
| `CONTACT_EMAIL` | The support email address to show on the app | - | `example@yoursite.com` |
| `CONTACT_EMAIL` | The support email address to show on the app | - | `example@yoursite.com` |
| `ALLOWED_TARGET_DOMAINS` | Whitelist of allowed target domains for URL shortening. Comma-separated list. Use `*.domain.com` to allow domain and all subdomains. If empty, all domains are allowed. | - | `*.github.qkg1.top,google.com` |

## Themes and customizations

Expand Down
1 change: 1 addition & 0 deletions server/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const spec = {
REPORT_EMAIL: str({ default: "" }),
CONTACT_EMAIL: str({ default: "" }),
NODE_APP_INSTANCE: num({ default: 0 }),
ALLOWED_TARGET_DOMAINS: str({ default: "" }),
};

for (const key in spec) {
Expand Down
9 changes: 6 additions & 3 deletions server/handlers/links.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ async function create(req, res) {
}),
!customurl && utils.generateId(query, domain_id),
validators.bannedDomain(targetDomain),
validators.bannedHost(targetDomain)
validators.bannedHost(targetDomain),
validators.allowedDomain(targetDomain)
]);

// if "reuse" is true, try to return
Expand Down Expand Up @@ -217,7 +218,8 @@ async function edit(req, res) {
domain_id
}),
target && validators.bannedDomain(targetDomain),
target && validators.bannedHost(targetDomain)
target && validators.bannedHost(targetDomain),
target && validators.allowedDomain(targetDomain)
]);

// Check if custom link already exists
Expand Down Expand Up @@ -310,7 +312,8 @@ async function editAdmin(req, res) {
domain_id
}),
target && validators.bannedDomain(targetDomain),
target && validators.bannedHost(targetDomain)
target && validators.bannedHost(targetDomain),
target && validators.allowedDomain(targetDomain)
]);

// Check if custom link already exists
Expand Down
27 changes: 27 additions & 0 deletions server/handlers/validators.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,36 @@ async function bannedHost(domain) {
}
};

function allowedDomain(domain) {
const allowedList = env.ALLOWED_TARGET_DOMAINS
.split(",")
.map(d => d.trim().toLowerCase())
.filter(Boolean);

if (allowedList.length === 0) return;

const normalizedDomain = domain.toLowerCase();

const isAllowed = allowedList.some(allowed => {
if (allowed.startsWith("*.")) {
const baseDomain = allowed.slice(2);
return normalizedDomain === baseDomain || normalizedDomain.endsWith("." + baseDomain);
}
return normalizedDomain === allowed;
});

if (!isAllowed) {
throw new utils.CustomError(
"This domain is not allowed. Only URLs from approved domains can be shortened.",
400
);
}
};

module.exports = {
addDomain,
addDomainAdmin,
allowedDomain,
banDomain,
banLink,
banUser,
Expand Down