@jwilleke/ngdpbase-types 0.0.0 → 5.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +16 -2
- package/lib/packages/ngdpbase-types/index.d.ts +32 -0
- package/lib/packages/ngdpbase-types/testing.d.ts +43 -0
- package/lib/packages/ngdpbase-types/testing.js +53 -0
- package/lib/src/cache/CacheDependencies.d.ts +50 -0
- package/lib/src/cache/CacheDependencies.js +50 -0
- package/lib/src/cache/ICacheAdapter.d.ts +86 -0
- package/lib/src/cache/RegionCache.d.ts +171 -0
- package/lib/src/context/ActorContext.d.ts +48 -0
- package/lib/src/context/ActorContext.js +33 -0
- package/lib/src/context/ApiContext.d.ts +194 -0
- package/lib/src/context/ApiContext.js +273 -0
- package/lib/src/context/BaseContext.d.ts +110 -0
- package/lib/src/context/BaseContext.js +143 -0
- package/lib/src/context/JobContext.d.ts +153 -0
- package/lib/src/context/JobContext.js +144 -0
- package/lib/src/context/WikiContext.d.ts +404 -0
- package/lib/src/context/bootActions.d.ts +64 -0
- package/lib/src/context/bootActions.js +111 -0
- package/lib/src/converters/HtmlConverter.d.ts +70 -0
- package/lib/src/converters/IContentConverter.d.ts +94 -0
- package/lib/src/converters/JSPWikiConverter.d.ts +117 -0
- package/lib/src/converters/conversionWarning.d.ts +15 -0
- package/lib/src/converters/ncm/bridge.d.ts +22 -0
- package/lib/src/converters/ncm/existing.d.ts +23 -0
- package/lib/src/converters/ncm/fix/blocks.d.ts +89 -0
- package/lib/src/converters/ncm/fix/bulletMarkers.d.ts +21 -0
- package/lib/src/converters/ncm/fix/index.d.ts +36 -0
- package/lib/src/converters/ncm/fix/jspwikiBullets.d.ts +20 -0
- package/lib/src/converters/ncm/fix/jspwikiCodeMarkers.d.ts +38 -0
- package/lib/src/converters/ncm/fix/jspwikiHeadings.d.ts +25 -0
- package/lib/src/converters/ncm/fix/jspwikiIndent.d.ts +24 -0
- package/lib/src/converters/ncm/fix/jspwikiInsert.d.ts +21 -0
- package/lib/src/converters/ncm/fix/jspwikiItalic.d.ts +29 -0
- package/lib/src/converters/ncm/fix/jspwikiLinkLeftovers.d.ts +27 -0
- package/lib/src/converters/ncm/fix/moreInformationFooter.d.ts +25 -0
- package/lib/src/converters/ncm/fix/styleClosers.d.ts +25 -0
- package/lib/src/converters/ncm/fix/tightenLists.d.ts +30 -0
- package/lib/src/converters/ncm/fix/types.d.ts +33 -0
- package/lib/src/converters/ncm/footnotes.d.ts +39 -0
- package/lib/src/converters/ncm/images.d.ts +49 -0
- package/lib/src/converters/ncm/index.d.ts +20 -0
- package/lib/src/converters/ncm/links.d.ts +21 -0
- package/lib/src/converters/ncm/normalize.d.ts +39 -0
- package/lib/src/converters/ncm/placeholder.d.ts +25 -0
- package/lib/src/converters/ncm/tables.d.ts +36 -0
- package/lib/src/converters/ncm/types.d.ts +42 -0
- package/lib/src/http/egressPolicy.d.ts +52 -0
- package/lib/src/http/guardedFetch.d.ts +89 -0
- package/lib/src/http/guardedFetch.js +113 -0
- package/lib/src/http/guardedLookup.d.ts +48 -0
- package/lib/src/http/guardedLookup.js +73 -0
- package/lib/src/http/ssrf.d.ts +83 -0
- package/lib/src/http/ssrf.js +311 -0
- package/lib/src/mail/MailProvider.d.ts +36 -0
- package/lib/src/mail/NodemailerMailProvider.d.ts +26 -0
- package/lib/src/managers/AddonsManager.d.ts +691 -0
- package/lib/src/managers/AgentTokenManager.d.ts +269 -0
- package/lib/src/managers/AssetManager.d.ts +116 -0
- package/lib/src/managers/AttachmentManager.d.ts +1032 -0
- package/lib/src/managers/AuthManager.d.ts +1257 -0
- package/lib/src/managers/BackgroundJobManager.d.ts +434 -0
- package/lib/src/managers/BaseManager.d.ts +391 -0
- package/lib/src/managers/BaseManager.js +415 -0
- package/lib/src/managers/CacheManager.d.ts +159 -0
- package/lib/src/managers/CatalogManager.d.ts +235 -0
- package/lib/src/managers/CommentManager.d.ts +75 -0
- package/lib/src/managers/ConfigurationManager.d.ts +747 -0
- package/lib/src/managers/DatabaseManager.d.ts +89 -0
- package/lib/src/managers/DatabaseManager.js +236 -0
- package/lib/src/managers/EmailManager.d.ts +47 -0
- package/lib/src/managers/ExportManager.d.ts +129 -0
- package/lib/src/managers/ExportManager.js +420 -0
- package/lib/src/managers/FootnoteManager.d.ts +118 -0
- package/lib/src/managers/MediaManager.d.ts +279 -0
- package/lib/src/managers/MetricsManager.d.ts +89 -0
- package/lib/src/managers/NotificationManager.d.ts +165 -0
- package/lib/src/managers/OidcManager.d.ts +210 -0
- package/lib/src/managers/OrganizationManager.d.ts +107 -0
- package/lib/src/managers/PageManager.d.ts +1645 -0
- package/lib/src/managers/PersonManager.d.ts +48 -0
- package/lib/src/managers/PluginManager.d.ts +179 -0
- package/lib/src/managers/PluginManager.js +393 -0
- package/lib/src/managers/PolicyEvaluator.d.ts +213 -0
- package/lib/src/managers/RenderingManager.d.ts +358 -0
- package/lib/src/managers/RoleManager.d.ts +134 -0
- package/lib/src/managers/SearchManager.d.ts +402 -0
- package/lib/src/managers/ShareManager.d.ts +251 -0
- package/lib/src/managers/TemplateManager.d.ts +176 -0
- package/lib/src/managers/ThemeManager.d.ts +56 -0
- package/lib/src/managers/UserManager.d.ts +480 -0
- package/lib/src/managers/ValidationManager.d.ts +426 -0
- package/lib/src/managers/VariableManager.d.ts +145 -0
- package/lib/src/middleware/csrf.d.ts +3 -0
- package/lib/src/parsers/LinkParser.d.ts +457 -0
- package/lib/src/parsers/MarkupParser.d.ts +769 -0
- package/lib/src/parsers/context/ParseContext.d.ts +314 -0
- package/lib/src/parsers/data/emoji-map.d.ts +18 -0
- package/lib/src/parsers/dom/DOMBuilder.d.ts +210 -0
- package/lib/src/parsers/dom/DOMParser.d.ts +252 -0
- package/lib/src/parsers/dom/Tokenizer.d.ts +358 -0
- package/lib/src/parsers/dom/WikiDocument.d.ts +272 -0
- package/lib/src/parsers/dom/handlers/DOMLinkHandler.d.ts +260 -0
- package/lib/src/parsers/dom/handlers/DOMPluginHandler.d.ts +212 -0
- package/lib/src/parsers/dom/handlers/DOMVariableHandler.d.ts +171 -0
- package/lib/src/parsers/filters/BaseFilter.d.ts +269 -0
- package/lib/src/parsers/filters/FilterChain.d.ts +320 -0
- package/lib/src/parsers/handlers/BaseSyntaxHandler.d.ts +331 -0
- package/lib/src/parsers/handlers/HandlerRegistry.d.ts +225 -0
- package/lib/src/parsers/handlers/JSPWikiPreprocessor.d.ts +89 -0
- package/lib/src/parsers/handlers/LinkParserHandler.d.ts +80 -0
- package/lib/src/parsers/handlers/PluginSyntaxHandler.d.ts +78 -0
- package/lib/src/parsers/handlers/WikiFormHandler.d.ts +149 -0
- package/lib/src/parsers/handlers/WikiTagHandler.d.ts +125 -0
- package/lib/src/parsers/jspwikiTableRow.d.ts +39 -0
- package/lib/src/parsers/styleBlockSyntax.d.ts +14 -0
- package/lib/src/plugins/types.d.ts +79 -0
- package/lib/src/providers/AgentTokenAuthProvider.d.ts +40 -0
- package/lib/src/providers/AuthentikBearerAuthProvider.d.ts +75 -0
- package/lib/src/providers/BaseAttachmentProvider.d.ts +197 -0
- package/lib/src/providers/BaseAuthProvider.d.ts +220 -0
- package/lib/src/providers/BaseCacheProvider.d.ts +140 -0
- package/lib/src/providers/BaseCredentialsProvider.d.ts +84 -0
- package/lib/src/providers/BaseDatabaseProvider.d.ts +22 -0
- package/lib/src/providers/BaseDatabaseProvider.js +12 -0
- package/lib/src/providers/BaseLoggingProvider.d.ts +68 -0
- package/lib/src/providers/BaseLoggingProvider.js +37 -0
- package/lib/src/providers/BaseMediaProvider.d.ts +308 -0
- package/lib/src/providers/BasePageProvider.d.ts +337 -0
- package/lib/src/providers/BaseProvider.d.ts +98 -0
- package/lib/src/providers/BaseProvider.js +74 -0
- package/lib/src/providers/BaseSearchProvider.d.ts +362 -0
- package/lib/src/providers/BasicAttachmentProvider.d.ts +599 -0
- package/lib/src/providers/CloudflareAccessAuthProvider.d.ts +72 -0
- package/lib/src/providers/FileCredentialsProvider.d.ts +33 -0
- package/lib/src/providers/FileJobStateProvider.d.ts +58 -0
- package/lib/src/providers/FileLoggingProvider.d.ts +43 -0
- package/lib/src/providers/FileLoggingProvider.js +120 -0
- package/lib/src/providers/FileOidcAdapter.d.ts +25 -0
- package/lib/src/providers/FileSystemMediaProvider.d.ts +403 -0
- package/lib/src/providers/LunrSearchProvider.d.ts +280 -0
- package/lib/src/providers/MagicLinkAuthProvider.d.ts +125 -0
- package/lib/src/providers/NullCacheProvider.d.ts +72 -0
- package/lib/src/providers/OidcAuthProvider.d.ts +180 -0
- package/lib/src/providers/OidcBearerAuthProvider.d.ts +30 -0
- package/lib/src/providers/PasskeyAuthProvider.d.ts +58 -0
- package/lib/src/providers/PasswordAuthProvider.d.ts +20 -0
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +55 -0
- package/lib/src/providers/SqliteDatabaseProvider.js +90 -0
- package/lib/src/providers/SqliteJobStateProvider.d.ts +41 -0
- package/lib/src/providers/databaseMigrations.d.ts +8 -0
- package/lib/src/providers/databaseMigrations.js +47 -0
- package/lib/src/providers/jobState.d.ts +99 -0
- package/lib/src/providers/lunrQueryToMiniSearch.d.ts +32 -0
- package/lib/src/providers/mediaFilenameDate.d.ts +22 -0
- package/lib/src/providers/sqliteLocation.d.ts +17 -0
- package/lib/src/providers/sqliteLocation.js +55 -0
- package/lib/src/providers/sqliteMigrations.d.ts +48 -0
- package/lib/src/providers/sqliteMigrations.js +72 -0
- package/lib/src/rendering/githubAlerts.d.ts +22 -0
- package/lib/src/rendering/htmlPolicy.d.ts +76 -0
- package/lib/src/rendering/markdownConverter.d.ts +73 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +245 -0
- package/lib/src/security/PolicyInformationPoint.d.ts +448 -0
- package/lib/src/security/pageActions.d.ts +40 -0
- package/lib/src/security/permissions.generated.d.ts +78 -0
- package/lib/src/types/Asset.d.ts +414 -0
- package/lib/src/types/Catalog.d.ts +61 -0
- package/lib/src/types/Comment.d.ts +11 -0
- package/lib/src/types/Config.d.ts +388 -0
- package/lib/src/types/Organization.d.ts +78 -0
- package/lib/src/types/OrganizationProvider.d.ts +38 -0
- package/lib/src/types/Page.d.ts +220 -0
- package/lib/src/types/Person.d.ts +56 -0
- package/lib/src/types/PersonProvider.d.ts +25 -0
- package/lib/src/types/Policy.d.ts +139 -0
- package/lib/src/types/Policy.js +17 -0
- package/lib/src/types/Provider.d.ts +810 -0
- package/lib/src/types/Role.d.ts +84 -0
- package/lib/src/types/RoleProvider.d.ts +35 -0
- package/lib/src/types/Schema.d.ts +425 -0
- package/lib/src/types/Share.d.ts +172 -0
- package/lib/src/types/Share.js +81 -0
- package/lib/src/types/User.d.ts +304 -0
- package/lib/src/types/Version.d.ts +189 -0
- package/lib/src/types/WikiEngine.d.ts +128 -0
- package/lib/src/types/express.d.ts +120 -0
- package/lib/src/types/express.js +1 -0
- package/lib/src/types/guards.d.ts +78 -0
- package/lib/src/types/heic-decode.d.ts +16 -0
- package/lib/src/types/index.d.ts +19 -0
- package/lib/src/types/linkedom.d.ts +77 -0
- package/lib/src/types/markdown-it-plugins.d.ts +21 -0
- package/lib/src/utils/DeltaStorage.d.ts +118 -0
- package/lib/src/utils/LocaleUtils.d.ts +105 -0
- package/lib/src/utils/LocaleUtils.js +294 -0
- package/lib/src/utils/PageNameMatcher.d.ts +98 -0
- package/lib/src/utils/PathPreflight.d.ts +38 -0
- package/lib/src/utils/PathPreflight.js +50 -0
- package/lib/src/utils/SectionUtils.d.ts +40 -0
- package/lib/src/utils/addonConfigLayer.d.ts +144 -0
- package/lib/src/utils/addonConfigLayer.js +382 -0
- package/lib/src/utils/addonDatabaseRules.d.ts +25 -0
- package/lib/src/utils/addonDatabaseRules.js +57 -0
- package/lib/src/utils/addonHost.d.ts +51 -0
- package/lib/src/utils/addonHost.js +72 -0
- package/lib/src/utils/addonInstaller.d.ts +109 -0
- package/lib/src/utils/addonLock.d.ts +37 -0
- package/lib/src/utils/addonLock.js +48 -0
- package/lib/src/utils/addonPageSync.d.ts +45 -0
- package/lib/src/utils/addonProvenance.d.ts +48 -0
- package/lib/src/utils/addonTarball.d.ts +41 -0
- package/lib/src/utils/addonsPathResolver.d.ts +76 -0
- package/lib/src/utils/addonsPathResolver.js +129 -0
- package/lib/src/utils/assetMetadataTags.d.ts +66 -0
- package/lib/src/utils/atomicWrite.d.ts +28 -0
- package/lib/src/utils/auditConfigChange.d.ts +57 -0
- package/lib/src/utils/auditEventNames.d.ts +50 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +163 -0
- package/lib/src/utils/auditEventNames.generated.js +168 -0
- package/lib/src/utils/auditEventNames.js +49 -0
- package/lib/src/utils/auditEvents.d.ts +265 -0
- package/lib/src/utils/auditEvents.js +309 -0
- package/lib/src/utils/auditRegistry.d.ts +102 -0
- package/lib/src/utils/auditRegistry.js +168 -0
- package/lib/src/utils/bip39English.d.ts +2 -0
- package/lib/src/utils/configEnvKeys.d.ts +68 -0
- package/lib/src/utils/configFiles.d.ts +74 -0
- package/lib/src/utils/configFiles.js +150 -0
- package/lib/src/utils/delegation.d.ts +48 -0
- package/lib/src/utils/deriveUsername.d.ts +21 -0
- package/lib/src/utils/explainMediaPath.d.ts +123 -0
- package/lib/src/utils/formerTitles.d.ts +69 -0
- package/lib/src/utils/frontmatterAccess.d.ts +54 -0
- package/lib/src/utils/headlessAdminPassword.d.ts +55 -0
- package/lib/src/utils/imageTransform.d.ts +52 -0
- package/lib/src/utils/instanceEnvSecret.d.ts +51 -0
- package/lib/src/utils/jobSlots.d.ts +43 -0
- package/lib/src/utils/keywordNormalizer.d.ts +127 -0
- package/lib/src/utils/logger.d.ts +70 -0
- package/lib/src/utils/logger.js +106 -0
- package/lib/src/utils/magicLinkDeviceState.d.ts +95 -0
- package/lib/src/utils/managerUtils.d.ts +40 -0
- package/lib/src/utils/migrateLegacyPrivateAttachments.d.ts +15 -0
- package/lib/src/utils/orgFilename.d.ts +22 -0
- package/lib/src/utils/pageFeature.d.ts +27 -0
- package/lib/src/utils/pageFrontmatter.d.ts +21 -0
- package/lib/src/utils/pageNotes.d.ts +150 -0
- package/lib/src/utils/pageTitleRule.d.ts +37 -0
- package/lib/src/utils/pageToArticle.d.ts +59 -0
- package/lib/src/utils/pageUrl.d.ts +18 -0
- package/lib/src/utils/passwordChange.d.ts +12 -0
- package/lib/src/utils/passwordHash.d.ts +60 -0
- package/lib/src/utils/permissionCatalog.d.ts +52 -0
- package/lib/src/utils/pluginFormatters.d.ts +452 -0
- package/lib/src/utils/pluginFormatters.js +714 -0
- package/lib/src/utils/privateLinkRewrite.d.ts +42 -0
- package/lib/src/utils/privateStoreAccess.d.ts +57 -0
- package/lib/src/utils/privateStoreAccess.js +65 -0
- package/lib/src/utils/privateStoreCatalogs.d.ts +40 -0
- package/lib/src/utils/privateStoreCrypto.d.ts +127 -0
- package/lib/src/utils/privateStoreDoor.d.ts +181 -0
- package/lib/src/utils/privateStoreExport.d.ts +72 -0
- package/lib/src/utils/privateStoreFiles.d.ts +120 -0
- package/lib/src/utils/privateStoreMeta.d.ts +64 -0
- package/lib/src/utils/privateStoreMigrations.d.ts +37 -0
- package/lib/src/utils/privateStorePath.d.ts +259 -0
- package/lib/src/utils/privateStorePath.js +436 -0
- package/lib/src/utils/privateStoreTakeout.d.ts +76 -0
- package/lib/src/utils/privateStoreUnlock.d.ts +175 -0
- package/lib/src/utils/proxyClaim.d.ts +59 -0
- package/lib/src/utils/proxyClaim.js +30 -0
- package/lib/src/utils/redactPrivateNames.d.ts +21 -0
- package/lib/src/utils/redactPrivateNames.js +89 -0
- package/lib/src/utils/redactSecrets.d.ts +58 -0
- package/lib/src/utils/redactSecrets.js +171 -0
- package/lib/src/utils/renameLinkRewrite.d.ts +112 -0
- package/lib/src/utils/roleGrants.d.ts +35 -0
- package/lib/src/utils/safeRedirect.d.ts +32 -0
- package/lib/src/utils/schedule.d.ts +54 -0
- package/lib/src/utils/secretFileMode.d.ts +17 -0
- package/lib/src/utils/seededShippedPages.d.ts +45 -0
- package/lib/src/utils/sensitiveValues.d.ts +59 -0
- package/lib/src/utils/sensitiveValues.js +106 -0
- package/lib/src/utils/sessionEnd.d.ts +12 -0
- package/lib/src/utils/sessionGeneration.d.ts +24 -0
- package/lib/src/utils/sessionIdle.d.ts +41 -0
- package/lib/src/utils/sessionLimits.d.ts +68 -0
- package/lib/src/utils/shareLockbox.d.ts +64 -0
- package/lib/src/utils/sigstoreTrustedRoot.d.ts +33 -0
- package/lib/src/utils/sniffMime.d.ts +21 -0
- package/lib/src/utils/storeSearchIndex.d.ts +75 -0
- package/lib/src/utils/subjectMayDo.d.ts +21 -0
- package/lib/src/utils/systemCategoryRules.d.ts +30 -0
- package/lib/src/utils/systemCategoryRules.js +41 -0
- package/lib/src/utils/userCreateError.d.ts +62 -0
- package/lib/src/utils/username.d.ts +53 -0
- package/lib/src/utils/vaultUrlRewrite.d.ts +27 -0
- package/lib/src/utils/version.d.ts +84 -0
- package/lib/src/utils/videoFrame.d.ts +21 -0
- package/package.json +54 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guarded resolver (#1133) — the actual SSRF control.
|
|
3
|
+
*
|
|
4
|
+
* `validateUrl` in ./ssrf.ts inspects a URL string and is a courtesy: it gives
|
|
5
|
+
* an operator a clear message instead of a connection error. It cannot be the
|
|
6
|
+
* boundary, because a name that resolves to a public address when validated
|
|
7
|
+
* can resolve to an internal one moments later when connected, and because a
|
|
8
|
+
* redirect sends the request somewhere the original URL never named.
|
|
9
|
+
*
|
|
10
|
+
* This runs at connect time, after resolution, for every connection an agent
|
|
11
|
+
* makes — which is every redirect hop as well.
|
|
12
|
+
*
|
|
13
|
+
* Adapted from `src/http/ssrf.ts` in jwilleke/yourphr, sole-authored by the
|
|
14
|
+
* copyright holder and contributed here under this repository's Apache-2.0
|
|
15
|
+
* licence. The callback-shape handling and the `all: true` reasoning carry
|
|
16
|
+
* over; taking an `EgressPolicy` rather than a boolean, and the injected
|
|
17
|
+
* resolver, are new.
|
|
18
|
+
*/
|
|
19
|
+
import { type LookupAddress } from 'node:dns';
|
|
20
|
+
import { type EgressPolicy } from './ssrf.js';
|
|
21
|
+
/** Marks a refusal as the guard's own decision rather than a network failure. */
|
|
22
|
+
export declare const ESSRF_BLOCKED = "ESSRFBLOCKED";
|
|
23
|
+
/**
|
|
24
|
+
* The resolution step, injectable.
|
|
25
|
+
*
|
|
26
|
+
* Production passes `node:dns`'s `lookup`. Tests pass a stub, which is how the
|
|
27
|
+
* plumbing is exercised without DNS or a socket — and deliberately the ONLY
|
|
28
|
+
* seam: tier 1 in ./ssrf.ts is absolute, so no policy can open loopback to
|
|
29
|
+
* reach a fixture server, and a production flag that could is precisely the
|
|
30
|
+
* switch that gets left on. The seam is a parameter a caller must supply, not
|
|
31
|
+
* a mode the shipped module can be put into.
|
|
32
|
+
*/
|
|
33
|
+
export type Resolver = (hostname: string, options: {
|
|
34
|
+
all: true;
|
|
35
|
+
}, callback: (err: NodeJS.ErrnoException | null, addresses?: LookupAddress[]) => void) => void;
|
|
36
|
+
/**
|
|
37
|
+
* A `dns.lookup` that refuses to hand back an address the policy does not permit.
|
|
38
|
+
*
|
|
39
|
+
* Pass as the `lookup` option to an `http.Agent` / `https.Agent`; Node then
|
|
40
|
+
* calls it for every connection that agent opens.
|
|
41
|
+
*
|
|
42
|
+
* Resolution is always requested with `all: true` regardless of what the
|
|
43
|
+
* caller asked for, and EVERY returned address is judged. A name with several
|
|
44
|
+
* A records must not pass on the strength of one public answer while another
|
|
45
|
+
* points inward — that is round-robin SSRF, and checking only the first record
|
|
46
|
+
* misses it entirely. The caller's requested shape is restored afterwards.
|
|
47
|
+
*/
|
|
48
|
+
export declare function guardedLookup(policy: EgressPolicy, resolver?: Resolver): (hostname: string, options: unknown, callback?: unknown) => void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guarded resolver (#1133) — the actual SSRF control.
|
|
3
|
+
*
|
|
4
|
+
* `validateUrl` in ./ssrf.ts inspects a URL string and is a courtesy: it gives
|
|
5
|
+
* an operator a clear message instead of a connection error. It cannot be the
|
|
6
|
+
* boundary, because a name that resolves to a public address when validated
|
|
7
|
+
* can resolve to an internal one moments later when connected, and because a
|
|
8
|
+
* redirect sends the request somewhere the original URL never named.
|
|
9
|
+
*
|
|
10
|
+
* This runs at connect time, after resolution, for every connection an agent
|
|
11
|
+
* makes — which is every redirect hop as well.
|
|
12
|
+
*
|
|
13
|
+
* Adapted from `src/http/ssrf.ts` in jwilleke/yourphr, sole-authored by the
|
|
14
|
+
* copyright holder and contributed here under this repository's Apache-2.0
|
|
15
|
+
* licence. The callback-shape handling and the `all: true` reasoning carry
|
|
16
|
+
* over; taking an `EgressPolicy` rather than a boolean, and the injected
|
|
17
|
+
* resolver, are new.
|
|
18
|
+
*/
|
|
19
|
+
import { lookup as dnsLookup } from 'node:dns';
|
|
20
|
+
import { isAddressAllowed, isBlockedHostname, REFUSAL } from './ssrf.js';
|
|
21
|
+
/** Marks a refusal as the guard's own decision rather than a network failure. */
|
|
22
|
+
export const ESSRF_BLOCKED = 'ESSRFBLOCKED';
|
|
23
|
+
function refuse(message) {
|
|
24
|
+
return Object.assign(new Error(message), { code: ESSRF_BLOCKED });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A `dns.lookup` that refuses to hand back an address the policy does not permit.
|
|
28
|
+
*
|
|
29
|
+
* Pass as the `lookup` option to an `http.Agent` / `https.Agent`; Node then
|
|
30
|
+
* calls it for every connection that agent opens.
|
|
31
|
+
*
|
|
32
|
+
* Resolution is always requested with `all: true` regardless of what the
|
|
33
|
+
* caller asked for, and EVERY returned address is judged. A name with several
|
|
34
|
+
* A records must not pass on the strength of one public answer while another
|
|
35
|
+
* points inward — that is round-robin SSRF, and checking only the first record
|
|
36
|
+
* misses it entirely. The caller's requested shape is restored afterwards.
|
|
37
|
+
*/
|
|
38
|
+
export function guardedLookup(policy, resolver = dnsLookup) {
|
|
39
|
+
return function guarded(hostname, options, callback) {
|
|
40
|
+
// Node calls lookup(hostname, options, cb) or lookup(hostname, cb).
|
|
41
|
+
const cb = (typeof options === 'function' ? options : callback);
|
|
42
|
+
const opts = (typeof options === 'function' ? {} : options ?? {});
|
|
43
|
+
// Names that are internal by definition never reach the resolver.
|
|
44
|
+
if (isBlockedHostname(hostname)) {
|
|
45
|
+
cb(refuse(`${REFUSAL}: ${hostname}`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
resolver(hostname, { ...opts, all: true }, (err, addresses) => {
|
|
49
|
+
if (err) {
|
|
50
|
+
cb(err);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const resolved = addresses ?? [];
|
|
54
|
+
const blocked = resolved.find((a) => !isAddressAllowed(a.address, policy));
|
|
55
|
+
if (blocked) {
|
|
56
|
+
cb(refuse(`${REFUSAL}: ${hostname} resolved to ${blocked.address}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (opts['all'] === true) {
|
|
60
|
+
cb(null, resolved);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const first = resolved[0];
|
|
64
|
+
if (!first) {
|
|
65
|
+
// An empty answer must be an error, not `undefined` handed onward as
|
|
66
|
+
// though it were an address.
|
|
67
|
+
cb(Object.assign(new Error(`no addresses for ${hostname}`), { code: 'ENOTFOUND' }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
cb(null, first.address, first.family);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Message every refusal carries, so callers and tests recognise the guard's own decision. */
|
|
2
|
+
export declare const REFUSAL = "refusing to connect to an internal address";
|
|
3
|
+
/** The operator-settable half of the boundary. Both lists hold IPv4 and IPv6 CIDRs. */
|
|
4
|
+
export interface EgressPolicy {
|
|
5
|
+
/** Ranges refused in addition to the defaults. */
|
|
6
|
+
deniedRanges: string[];
|
|
7
|
+
/** Ranges permitted despite the defaults — e.g. a home instance opening its own LAN. */
|
|
8
|
+
allowedRanges: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The host as an address string, with the decorations URL parsing leaves on.
|
|
12
|
+
*
|
|
13
|
+
* `new URL('http://[::1]/').hostname` is the string `"[::1]"`, and
|
|
14
|
+
* `net.isIP('[::1]')` is 0 — so a literal-address check that skips the
|
|
15
|
+
* brackets concludes "not an address" and lets every IPv6 literal through.
|
|
16
|
+
* That was measured reaching a live local service.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeHost(hostname: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Names that never leave the machine, refused before any resolution.
|
|
21
|
+
*
|
|
22
|
+
* The suffixes are the ones mDNS, container runtimes and cloud providers use
|
|
23
|
+
* for names that are internal by definition.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isBlockedHostname(host: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Tier 1 only: is this an address no configuration may permit?
|
|
28
|
+
*
|
|
29
|
+
* Separate from `isAddressAllowed` so the mechanism can be asserted on its own
|
|
30
|
+
* — a profile or an allow-list that could reach these would be a control that
|
|
31
|
+
* can be switched off, which is not a control.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isMechanismDenied(address: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The full decision for one address.
|
|
36
|
+
*
|
|
37
|
+
* Order, and the reasoning behind it:
|
|
38
|
+
*
|
|
39
|
+
* 1. Unparseable -> refuse. Fail closed.
|
|
40
|
+
* 2. Tier 1 -> refuse, whatever the policy says.
|
|
41
|
+
* 3. Explicit policy, longest prefix wins; a tie goes to deny. Explicit
|
|
42
|
+
* entries outrank the built-in defaults at equal length, mirroring the
|
|
43
|
+
* profile rule that an explicit key always beats a preset (#1137).
|
|
44
|
+
* 4. Built-in policy defaults -> refuse.
|
|
45
|
+
* 5. Otherwise public -> allow.
|
|
46
|
+
*
|
|
47
|
+
* Longest-prefix is the ROUTING convention, chosen deliberately over the
|
|
48
|
+
* access-control conventions (iptables, Cisco ACLs and AWS NACLs all resolve
|
|
49
|
+
* by rule order). It is used here because the values are routes: specificity
|
|
50
|
+
* is intrinsic to a CIDR and needs no operator-maintained ordering, which in a
|
|
51
|
+
* JSON config would drift silently.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isAddressAllowed(address: string, policy: EgressPolicy): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The early, friendly check. NOT the boundary — see the file header.
|
|
56
|
+
*
|
|
57
|
+
* A host that is not a literal address passes here and is judged at connect
|
|
58
|
+
* time instead, which is the only place a name's resolution can be seen.
|
|
59
|
+
*/
|
|
60
|
+
export declare function validateUrl(raw: string, policy: EgressPolicy): {
|
|
61
|
+
ok: true;
|
|
62
|
+
url: URL;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
reason: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Reconcile the two configured lists at BOOT, so a contradiction is a startup
|
|
69
|
+
* message rather than one dropped image months later — which would surface as
|
|
70
|
+
* a `fetch` warning indistinguishable from the remote host being down.
|
|
71
|
+
*
|
|
72
|
+
* An overlap between the lists is the normal case and not a conflict: the
|
|
73
|
+
* allow-list exists to narrow the deny-list. Only these are contradictions.
|
|
74
|
+
*/
|
|
75
|
+
export interface ReconciledPolicy {
|
|
76
|
+
/** Every contradiction, human-readable. */
|
|
77
|
+
conflicts: string[];
|
|
78
|
+
/** Ranges that do not parse as CIDR. No safe silent resolution — see #1152. */
|
|
79
|
+
malformed: string[];
|
|
80
|
+
/** Ranges present verbatim in both lists. A prefix-length tie; the deny wins. */
|
|
81
|
+
duplicates: string[];
|
|
82
|
+
}
|
|
83
|
+
export declare function reconcilePolicy(deniedRanges: readonly string[], allowedRanges: readonly string[]): ReconciledPolicy;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outbound address boundary (#1133).
|
|
3
|
+
*
|
|
4
|
+
* Three places in core called `fetch()` on a URL the server did not choose,
|
|
5
|
+
* with no host validation and `redirect: 'follow'`. The most reachable was
|
|
6
|
+
* gated on the page's own edit ACL, so anyone who could edit a page could make
|
|
7
|
+
* the server issue an HTTP request from inside the network.
|
|
8
|
+
*
|
|
9
|
+
* Adapted from `src/http/ssrf.ts` in jwilleke/yourphr — authored solely by the
|
|
10
|
+
* copyright holder and contributed here under this repository's Apache-2.0
|
|
11
|
+
* licence. Its numeric-host and bracket handling are carried over; the
|
|
12
|
+
* mechanism/policy tiering, the CIDR policy lists and the IPv6 transition
|
|
13
|
+
* ranges are new.
|
|
14
|
+
*
|
|
15
|
+
* THE REAL BOUNDARY IS AT CONNECT TIME, AFTER DNS. Nothing here is the control
|
|
16
|
+
* on its own — `validateUrl` is a courtesy that produces a friendly error
|
|
17
|
+
* early, and the guarded lookup that runs for every connection (including each
|
|
18
|
+
* redirect hop) is what actually holds. A URL check cannot be the boundary,
|
|
19
|
+
* because a name resolving to a public address at validation time can resolve
|
|
20
|
+
* to an internal one moments later at connect time.
|
|
21
|
+
*
|
|
22
|
+
* These are pure functions on purpose: the rules have to be checkable without
|
|
23
|
+
* a socket, an engine or a filesystem.
|
|
24
|
+
*/
|
|
25
|
+
import ipaddr from 'ipaddr.js';
|
|
26
|
+
/** Message every refusal carries, so callers and tests recognise the guard's own decision. */
|
|
27
|
+
export const REFUSAL = 'refusing to connect to an internal address';
|
|
28
|
+
/**
|
|
29
|
+
* Tier 1 — the mechanism. Never overridable, by an allow-list or a profile.
|
|
30
|
+
*
|
|
31
|
+
* These are the addresses an editor cannot otherwise reach. On a LAN instance
|
|
32
|
+
* the editor is already on the LAN, so the server reaching 192.168.x grants
|
|
33
|
+
* them nothing new — but `127.0.0.1` on the server is a capability they do not
|
|
34
|
+
* have, and that is the actual privilege boundary.
|
|
35
|
+
*
|
|
36
|
+
* Teredo is here rather than in the policy tier because its embedded IPv4 is
|
|
37
|
+
* obfuscated and so cannot be decoded and judged the way NAT64 and 6to4 are.
|
|
38
|
+
* The range is a legacy transition mechanism with no innocent use here.
|
|
39
|
+
*/
|
|
40
|
+
const MECHANISM_DENIED = [
|
|
41
|
+
'0.0.0.0/8', // "this network", includes the unspecified address
|
|
42
|
+
'127.0.0.0/8', // loopback
|
|
43
|
+
'169.254.0.0/16', // link-local, includes the 169.254.169.254 metadata address
|
|
44
|
+
'224.0.0.0/4', // multicast
|
|
45
|
+
'240.0.0.0/4', // reserved
|
|
46
|
+
'::/128', // unspecified
|
|
47
|
+
'::1/128', // loopback
|
|
48
|
+
'fe80::/10', // link-local
|
|
49
|
+
'ff00::/8', // multicast
|
|
50
|
+
'2001::/32' // Teredo — undecodable, denied wholesale
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Tier 2 — policy. Denied by default, and an allow-list entry may override it.
|
|
54
|
+
*
|
|
55
|
+
* This is the tier a home deployment typically opens to reach its own NAS or
|
|
56
|
+
* another host on the same segment — what the `baseline` recommendation
|
|
57
|
+
* describes, and which the operator opens themselves by setting the allow-list.
|
|
58
|
+
* Nothing here reads a posture name.
|
|
59
|
+
*/
|
|
60
|
+
const DEFAULT_POLICY_DENIED = [
|
|
61
|
+
'10.0.0.0/8', // RFC1918
|
|
62
|
+
'172.16.0.0/12', // RFC1918
|
|
63
|
+
'192.168.0.0/16', // RFC1918
|
|
64
|
+
'100.64.0.0/10', // RFC6598 carrier-grade NAT
|
|
65
|
+
'fc00::/7' // unique-local, includes fd00:ec2::254
|
|
66
|
+
];
|
|
67
|
+
const MECHANISM_CIDRS = MECHANISM_DENIED.map((c) => ipaddr.parseCIDR(c));
|
|
68
|
+
const DEFAULT_POLICY_CIDRS = DEFAULT_POLICY_DENIED.map((c) => ipaddr.parseCIDR(c));
|
|
69
|
+
/**
|
|
70
|
+
* The host as an address string, with the decorations URL parsing leaves on.
|
|
71
|
+
*
|
|
72
|
+
* `new URL('http://[::1]/').hostname` is the string `"[::1]"`, and
|
|
73
|
+
* `net.isIP('[::1]')` is 0 — so a literal-address check that skips the
|
|
74
|
+
* brackets concludes "not an address" and lets every IPv6 literal through.
|
|
75
|
+
* That was measured reaching a live local service.
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeHost(hostname) {
|
|
78
|
+
let host = hostname.trim().toLowerCase();
|
|
79
|
+
if (host.startsWith('[') && host.endsWith(']')) {
|
|
80
|
+
host = host.slice(1, -1);
|
|
81
|
+
}
|
|
82
|
+
const zone = host.indexOf('%');
|
|
83
|
+
return zone === -1 ? host : host.slice(0, zone);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Names that never leave the machine, refused before any resolution.
|
|
87
|
+
*
|
|
88
|
+
* The suffixes are the ones mDNS, container runtimes and cloud providers use
|
|
89
|
+
* for names that are internal by definition.
|
|
90
|
+
*/
|
|
91
|
+
export function isBlockedHostname(host) {
|
|
92
|
+
const h = host.trim().toLowerCase().replace(/\.$/, '');
|
|
93
|
+
if (h === 'localhost')
|
|
94
|
+
return true;
|
|
95
|
+
return ['.localhost', '.local', '.internal'].some((suffix) => h.endsWith(suffix));
|
|
96
|
+
}
|
|
97
|
+
/** Build an IPv4 address from four octets, or null if any is out of range. */
|
|
98
|
+
function ipv4From(octets) {
|
|
99
|
+
return octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)
|
|
100
|
+
? ipaddr.fromByteArray(octets)
|
|
101
|
+
: null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse an address, unwrapping the IPv6 forms that carry an IPv4 destination.
|
|
105
|
+
*
|
|
106
|
+
* Returns null for anything unparseable, which callers treat as a refusal: an
|
|
107
|
+
* address we cannot parse is not one we can vouch for, and the alternative is
|
|
108
|
+
* connecting blind.
|
|
109
|
+
*
|
|
110
|
+
* `ipaddr.js` 2 decodes the short and numeric forms this comment used to say it
|
|
111
|
+
* rejected — `127.1`, `0x7f.1`, `2130706433` all now parse, with the same octal
|
|
112
|
+
* and hex reading the system resolver uses. The verdict on each is unchanged,
|
|
113
|
+
* because what refuses them is the loopback range, not a parse failure; the
|
|
114
|
+
* difference is that the guard now judges the address the resolver would reach
|
|
115
|
+
* instead of refusing a string it could not read. That closes the differential
|
|
116
|
+
* rather than covering it, so the null path here is the narrower fallback it
|
|
117
|
+
* was always meant to be.
|
|
118
|
+
*/
|
|
119
|
+
function canonicalise(address) {
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = ipaddr.parse(normalizeHost(address));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (parsed.kind() !== 'ipv6')
|
|
128
|
+
return parsed;
|
|
129
|
+
const v6 = parsed;
|
|
130
|
+
// ::ffff:0:0/96 — the documented bypass when a guard reads only the text.
|
|
131
|
+
if (v6.isIPv4MappedAddress())
|
|
132
|
+
return v6.toIPv4Address();
|
|
133
|
+
const p = v6.parts;
|
|
134
|
+
// 64:ff9b::/96 — NAT64. The last 32 bits are the IPv4 destination, so on a
|
|
135
|
+
// NAT64 deployment 64:ff9b::a9fe:a9fe reaches 169.254.169.254.
|
|
136
|
+
if (p[0] === 0x0064 && p[1] === 0xff9b && p[2] === 0 && p[3] === 0 && p[4] === 0 && p[5] === 0) {
|
|
137
|
+
const v4 = ipv4From([p[6] >> 8, p[6] & 0xff, p[7] >> 8, p[7] & 0xff]);
|
|
138
|
+
if (v4)
|
|
139
|
+
return v4;
|
|
140
|
+
}
|
|
141
|
+
// 2002::/16 — 6to4. The IPv4 address sits in the second and third groups.
|
|
142
|
+
if (p[0] === 0x2002) {
|
|
143
|
+
const v4 = ipv4From([p[1] >> 8, p[1] & 0xff, p[2] >> 8, p[2] & 0xff]);
|
|
144
|
+
if (v4)
|
|
145
|
+
return v4;
|
|
146
|
+
}
|
|
147
|
+
return v6;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Does `addr` fall inside `cidr`?
|
|
151
|
+
*
|
|
152
|
+
* `ipaddr.parse` returns `IPv4 | IPv6`, and the two `match` signatures do not
|
|
153
|
+
* unify — so the families are narrowed apart rather than cast across. A
|
|
154
|
+
* cross-family comparison is false, never an error: an IPv4 address is not in
|
|
155
|
+
* an IPv6 range, and mapped forms were already unwrapped by `canonicalise`.
|
|
156
|
+
*/
|
|
157
|
+
function inRange(addr, cidr) {
|
|
158
|
+
if (addr.kind() !== cidr[0].kind())
|
|
159
|
+
return false;
|
|
160
|
+
return addr.kind() === 'ipv4'
|
|
161
|
+
? addr.match(cidr)
|
|
162
|
+
: addr.match(cidr);
|
|
163
|
+
}
|
|
164
|
+
/** Longest prefix among `cidrs` that contains `addr`, or null when none does. */
|
|
165
|
+
function longestMatch(addr, cidrs) {
|
|
166
|
+
let best = null;
|
|
167
|
+
for (const cidr of cidrs) {
|
|
168
|
+
if (inRange(addr, cidr) && (best === null || cidr[1] > best)) {
|
|
169
|
+
best = cidr[1];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return best;
|
|
173
|
+
}
|
|
174
|
+
/** Parse a CIDR list, silently dropping malformed entries — `reconcilePolicy` reports those. */
|
|
175
|
+
function parseList(ranges) {
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const range of ranges) {
|
|
178
|
+
try {
|
|
179
|
+
out.push(ipaddr.parseCIDR(range));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Reported at boot by reconcilePolicy; ignoring it here would be the
|
|
183
|
+
// only place a malformed range could widen access.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Tier 1 only: is this an address no configuration may permit?
|
|
190
|
+
*
|
|
191
|
+
* Separate from `isAddressAllowed` so the mechanism can be asserted on its own
|
|
192
|
+
* — a profile or an allow-list that could reach these would be a control that
|
|
193
|
+
* can be switched off, which is not a control.
|
|
194
|
+
*/
|
|
195
|
+
export function isMechanismDenied(address) {
|
|
196
|
+
const addr = canonicalise(address);
|
|
197
|
+
if (!addr)
|
|
198
|
+
return true;
|
|
199
|
+
return longestMatch(addr, MECHANISM_CIDRS) !== null;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The full decision for one address.
|
|
203
|
+
*
|
|
204
|
+
* Order, and the reasoning behind it:
|
|
205
|
+
*
|
|
206
|
+
* 1. Unparseable -> refuse. Fail closed.
|
|
207
|
+
* 2. Tier 1 -> refuse, whatever the policy says.
|
|
208
|
+
* 3. Explicit policy, longest prefix wins; a tie goes to deny. Explicit
|
|
209
|
+
* entries outrank the built-in defaults at equal length, mirroring the
|
|
210
|
+
* profile rule that an explicit key always beats a preset (#1137).
|
|
211
|
+
* 4. Built-in policy defaults -> refuse.
|
|
212
|
+
* 5. Otherwise public -> allow.
|
|
213
|
+
*
|
|
214
|
+
* Longest-prefix is the ROUTING convention, chosen deliberately over the
|
|
215
|
+
* access-control conventions (iptables, Cisco ACLs and AWS NACLs all resolve
|
|
216
|
+
* by rule order). It is used here because the values are routes: specificity
|
|
217
|
+
* is intrinsic to a CIDR and needs no operator-maintained ordering, which in a
|
|
218
|
+
* JSON config would drift silently.
|
|
219
|
+
*/
|
|
220
|
+
export function isAddressAllowed(address, policy) {
|
|
221
|
+
const addr = canonicalise(address);
|
|
222
|
+
if (!addr)
|
|
223
|
+
return false;
|
|
224
|
+
if (longestMatch(addr, MECHANISM_CIDRS) !== null)
|
|
225
|
+
return false;
|
|
226
|
+
const deniedPrefix = longestMatch(addr, parseList(policy.deniedRanges));
|
|
227
|
+
const allowedPrefix = longestMatch(addr, parseList(policy.allowedRanges));
|
|
228
|
+
if (deniedPrefix !== null || allowedPrefix !== null) {
|
|
229
|
+
if (allowedPrefix === null)
|
|
230
|
+
return false;
|
|
231
|
+
if (deniedPrefix === null)
|
|
232
|
+
return true;
|
|
233
|
+
return allowedPrefix > deniedPrefix;
|
|
234
|
+
}
|
|
235
|
+
return longestMatch(addr, DEFAULT_POLICY_CIDRS) === null;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The early, friendly check. NOT the boundary — see the file header.
|
|
239
|
+
*
|
|
240
|
+
* A host that is not a literal address passes here and is judged at connect
|
|
241
|
+
* time instead, which is the only place a name's resolution can be seen.
|
|
242
|
+
*/
|
|
243
|
+
export function validateUrl(raw, policy) {
|
|
244
|
+
let url;
|
|
245
|
+
try {
|
|
246
|
+
url = new URL(raw);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return { ok: false, reason: 'not a valid URL' };
|
|
250
|
+
}
|
|
251
|
+
// An exact allow-list, not `startsWith('http')` — which accepts `httpfoo:`
|
|
252
|
+
// and happens to be safe only because fetch rejects unknown schemes.
|
|
253
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
254
|
+
return { ok: false, reason: `unsupported scheme ${url.protocol}` };
|
|
255
|
+
}
|
|
256
|
+
const host = normalizeHost(url.hostname);
|
|
257
|
+
if (isBlockedHostname(host)) {
|
|
258
|
+
return { ok: false, reason: `${REFUSAL}: ${host}` };
|
|
259
|
+
}
|
|
260
|
+
if (canonicalise(host) && !isAddressAllowed(host, policy)) {
|
|
261
|
+
return { ok: false, reason: `${REFUSAL}: ${host}` };
|
|
262
|
+
}
|
|
263
|
+
return { ok: true, url };
|
|
264
|
+
}
|
|
265
|
+
export function reconcilePolicy(deniedRanges, allowedRanges) {
|
|
266
|
+
const conflicts = [];
|
|
267
|
+
// #1144: the three flagged cases are not equivalent, and a caller that can
|
|
268
|
+
// only see message strings cannot tell them apart. `malformed` has no safe
|
|
269
|
+
// silent resolution — dropping a malformed DENY fails open — while the other
|
|
270
|
+
// two resolve by convention (D8).
|
|
271
|
+
const malformed = [];
|
|
272
|
+
const duplicates = [];
|
|
273
|
+
for (const [label, ranges] of [['denied-ranges', deniedRanges], ['allowed-ranges', allowedRanges]]) {
|
|
274
|
+
for (const range of ranges) {
|
|
275
|
+
try {
|
|
276
|
+
ipaddr.parseCIDR(range);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
conflicts.push(`${label}: '${range}' is not a valid CIDR range`);
|
|
280
|
+
malformed.push(range);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const range of allowedRanges) {
|
|
285
|
+
let cidr;
|
|
286
|
+
try {
|
|
287
|
+
cidr = ipaddr.parseCIDR(range);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
continue; // already reported above
|
|
291
|
+
}
|
|
292
|
+
const hits = MECHANISM_CIDRS.filter((m) => cidrsIntersect(cidr, m));
|
|
293
|
+
if (hits.length > 0) {
|
|
294
|
+
conflicts.push(`allowed-ranges: '${range}' covers an address range that is never permitted ` +
|
|
295
|
+
`(${hits.map(([a, p]) => `${a.toString()}/${p}`).join(', ')}). ` +
|
|
296
|
+
'Loopback, link-local and multicast are the mechanism, not policy.');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const deniedSet = new Set(deniedRanges);
|
|
300
|
+
for (const range of allowedRanges) {
|
|
301
|
+
if (deniedSet.has(range)) {
|
|
302
|
+
conflicts.push(`'${range}' appears verbatim in both denied-ranges and allowed-ranges — the deny wins`);
|
|
303
|
+
duplicates.push(range);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { conflicts, malformed, duplicates };
|
|
307
|
+
}
|
|
308
|
+
/** Do two CIDRs overlap at all? True when either network address falls inside the other. */
|
|
309
|
+
function cidrsIntersect(a, b) {
|
|
310
|
+
return inRange(a[0], b) || inRange(b[0], a);
|
|
311
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MailProvider — pluggable email transport interface.
|
|
3
|
+
*
|
|
4
|
+
* Implementations:
|
|
5
|
+
* ConsoleMailProvider — prints to server log (development / no-SMTP installs)
|
|
6
|
+
* NodemailerMailProvider — sends via SMTP using nodemailer
|
|
7
|
+
*
|
|
8
|
+
* The active transport is selected by the
|
|
9
|
+
* `ngdpbase.mail.provider` config key ('console' | 'smtp').
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* An outbound email message.
|
|
13
|
+
*/
|
|
14
|
+
export interface MailMessage {
|
|
15
|
+
/** Recipient address */
|
|
16
|
+
to: string;
|
|
17
|
+
/** Email subject line */
|
|
18
|
+
subject: string;
|
|
19
|
+
/** Plain-text body */
|
|
20
|
+
text: string;
|
|
21
|
+
/** Optional HTML body */
|
|
22
|
+
html?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Common interface for all mail transports.
|
|
26
|
+
*/
|
|
27
|
+
export interface MailProvider {
|
|
28
|
+
send(message: MailMessage): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Development transport — writes the email to the server log instead of
|
|
32
|
+
* sending it. Useful for local installs where no SMTP server is available.
|
|
33
|
+
*/
|
|
34
|
+
export declare class ConsoleMailProvider implements MailProvider {
|
|
35
|
+
send(message: MailMessage): Promise<void>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodemailerMailProvider — SMTP mail transport using nodemailer.
|
|
3
|
+
*
|
|
4
|
+
* Configure via app-custom-config.json:
|
|
5
|
+
* ngdpbase.mail.provider.smtp.host
|
|
6
|
+
* ngdpbase.mail.provider.smtp.port
|
|
7
|
+
* ngdpbase.mail.provider.smtp.secure
|
|
8
|
+
* ngdpbase.mail.provider.smtp.user
|
|
9
|
+
* ngdpbase.mail.provider.smtp.pass
|
|
10
|
+
* ngdpbase.mail.provider.smtp.from
|
|
11
|
+
*/
|
|
12
|
+
import type { MailMessage, MailProvider } from './MailProvider.js';
|
|
13
|
+
export interface SmtpConfig {
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
secure: boolean;
|
|
17
|
+
user?: string;
|
|
18
|
+
pass?: string;
|
|
19
|
+
from: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class NodemailerMailProvider implements MailProvider {
|
|
22
|
+
private config;
|
|
23
|
+
private transporter;
|
|
24
|
+
constructor(config: SmtpConfig);
|
|
25
|
+
send(message: MailMessage): Promise<void>;
|
|
26
|
+
}
|