@@ -1315,4 +1315,182 @@ public void ItShouldFreezeModifiableProperties()
13151315
13161316 Assert . Throws < NotSupportedException > ( ( ) => sut . Extensions . Add ( "x-fake" , "fake_value" ) ) ;
13171317 }
1318+
1319+ [ Fact ]
1320+ public void ItShouldEncodeServerVariableValuesFromArguments ( )
1321+ {
1322+ // Arrange — variable value contains path-manipulation characters
1323+ var version = new RestApiServerVariable ( "v1" , null , [ "v1" , "v2/../admin" ] ) ;
1324+ var sut = new RestApiOperation (
1325+ id : "fake_id" ,
1326+ servers : [
1327+ new RestApiServer ( "https://example.com/{version}" , new Dictionary < string , RestApiServerVariable > { { "version" , version } } ) ,
1328+ ] ,
1329+ path : "/items" ,
1330+ method : HttpMethod . Get ,
1331+ description : "fake_description" ,
1332+ parameters : [ ] ,
1333+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
1334+ securityRequirements : [ ]
1335+ ) ;
1336+
1337+ var arguments = new Dictionary < string , object ? > ( ) { { "version" , "v2/../admin" } } ;
1338+
1339+ // Act
1340+ var url = sut . BuildOperationUrl ( arguments ) ;
1341+
1342+ // Assert — reserved separators (/) must be percent-encoded so they are not interpreted as path delimiters
1343+ Assert . Equal ( "https://example.com/v2%2F..%2Fadmin/items" , url . OriginalString ) ;
1344+ }
1345+
1346+ [ Fact ]
1347+ public void ItShouldPreventServerVariableInjectionWithSpecialCharacters ( )
1348+ {
1349+ // Arrange — variable value contains path traversal and query string injection
1350+ var host = new RestApiServerVariable ( "api.example.com" ) ;
1351+ var sut = new RestApiOperation (
1352+ id : "fake_id" ,
1353+ servers : [
1354+ new RestApiServer ( "https://{host}/api" , new Dictionary < string , RestApiServerVariable > { { "host" , host } } ) ,
1355+ ] ,
1356+ path : "/data" ,
1357+ method : HttpMethod . Get ,
1358+ description : "fake_description" ,
1359+ parameters : [ ] ,
1360+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
1361+ securityRequirements : [ ]
1362+ ) ;
1363+
1364+ var arguments = new Dictionary < string , object ? > ( ) { { "host" , "evil.com/hijack?q=1#" } } ;
1365+
1366+ // Act & Assert — encoding turns /, ?, # into percent-encoded sequences (%2F, %3F, %23),
1367+ // which prevents them from being interpreted as structural URI delimiters.
1368+ // The Uri constructor rejects the resulting hostname, which is the desired outcome.
1369+ Assert . ThrowsAny < UriFormatException > ( ( ) => sut . BuildOperationUrl ( arguments ) ) ;
1370+ }
1371+
1372+ [ Fact ]
1373+ public void ItShouldRejectDotSegmentInPathParameter ( )
1374+ {
1375+ // Arrange — path parameter value is ".." (dot-segment traversal)
1376+ var parameters = new List < RestApiParameter > {
1377+ new (
1378+ name : "id" ,
1379+ type : "string" ,
1380+ isRequired : true ,
1381+ expand : false ,
1382+ location : RestApiParameterLocation . Path ,
1383+ style : RestApiParameterStyle . Simple )
1384+ } ;
1385+
1386+ var sut = new RestApiOperation (
1387+ id : "fake_id" ,
1388+ servers : [ new RestApiServer ( "https://example.com/api" ) ] ,
1389+ path : "/resources/{id}/details" ,
1390+ method : HttpMethod . Get ,
1391+ description : "fake_description" ,
1392+ parameters : parameters ,
1393+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
1394+ securityRequirements : [ ]
1395+ ) ;
1396+
1397+ var arguments = new Dictionary < string , object ? > { { "id" , ".." } } ;
1398+
1399+ // Act & Assert — dot-segments must be rejected
1400+ var ex = Assert . Throws < KernelException > ( ( ) => sut . BuildOperationUrl ( arguments ) ) ;
1401+ Assert . Contains ( "dot-segment" , ex . Message ) ;
1402+ }
1403+
1404+ [ Fact ]
1405+ public void ItShouldRejectSingleDotSegmentInPathParameter ( )
1406+ {
1407+ // Arrange — path parameter value is "." (single-dot segment)
1408+ var parameters = new List < RestApiParameter > {
1409+ new (
1410+ name : "id" ,
1411+ type : "string" ,
1412+ isRequired : true ,
1413+ expand : false ,
1414+ location : RestApiParameterLocation . Path ,
1415+ style : RestApiParameterStyle . Simple )
1416+ } ;
1417+
1418+ var sut = new RestApiOperation (
1419+ id : "fake_id" ,
1420+ servers : [ new RestApiServer ( "https://example.com/api" ) ] ,
1421+ path : "/resources/{id}/details" ,
1422+ method : HttpMethod . Get ,
1423+ description : "fake_description" ,
1424+ parameters : parameters ,
1425+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
1426+ securityRequirements : [ ]
1427+ ) ;
1428+
1429+ var arguments = new Dictionary < string , object ? > { { "id" , "." } } ;
1430+
1431+ // Act & Assert — single-dot segments must also be rejected
1432+ var ex = Assert . Throws < KernelException > ( ( ) => sut . BuildOperationUrl ( arguments ) ) ;
1433+ Assert . Contains ( "dot-segment" , ex . Message ) ;
1434+ }
1435+
1436+ [ Fact ]
1437+ public void ItShouldAllowDotsInNonSegmentPathParameterValues ( )
1438+ {
1439+ // Arrange — path parameter contains dots but is NOT a dot-segment (e.g., "file.txt")
1440+ var parameters = new List < RestApiParameter > {
1441+ new (
1442+ name : "filename" ,
1443+ type : "string" ,
1444+ isRequired : true ,
1445+ expand : false ,
1446+ location : RestApiParameterLocation . Path ,
1447+ style : RestApiParameterStyle . Simple )
1448+ } ;
1449+
1450+ var sut = new RestApiOperation (
1451+ id : "fake_id" ,
1452+ servers : [ new RestApiServer ( "https://example.com/api" ) ] ,
1453+ path : "/files/{filename}" ,
1454+ method : HttpMethod . Get ,
1455+ description : "fake_description" ,
1456+ parameters : parameters ,
1457+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
1458+ securityRequirements : [ ]
1459+ ) ;
1460+
1461+ var arguments = new Dictionary < string , object ? > { { "filename" , "report.v2.txt" } } ;
1462+
1463+ // Act
1464+ var url = sut . BuildOperationUrl ( arguments ) ;
1465+
1466+ // Assert — dots within normal filenames should work fine
1467+ Assert . Equal ( "https://example.com/api/files/report.v2.txt" , url . OriginalString ) ;
1468+ }
1469+
1470+ [ Fact ]
1471+ public void ItShouldEncodeServerVariableValuesLookedUpByArgumentName ( )
1472+ {
1473+ // Arrange — variable uses ArgumentName and the argument contains path-manipulation characters
1474+ var version = new RestApiServerVariable ( "v1" , null , [ "v1" , "v2/../admin" ] ) { ArgumentName = "alt_version" } ;
1475+ var sut = new RestApiOperation (
1476+ id : "fake_id" ,
1477+ servers : [
1478+ new RestApiServer ( "https://example.com/{version}" , new Dictionary < string , RestApiServerVariable > { { "version" , version } } ) ,
1479+ ] ,
1480+ path : "/items" ,
1481+ method : HttpMethod . Get ,
1482+ description : "fake_description" ,
1483+ parameters : [ ] ,
1484+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
1485+ securityRequirements : [ ]
1486+ ) ;
1487+
1488+ var arguments = new Dictionary < string , object ? > ( ) { { "alt_version" , "v2/../admin" } } ;
1489+
1490+ // Act
1491+ var url = sut . BuildOperationUrl ( arguments ) ;
1492+
1493+ // Assert — reserved separators must be percent-encoded even when looked up via ArgumentName
1494+ Assert . Equal ( "https://example.com/v2%2F..%2Fadmin/items" , url . OriginalString ) ;
1495+ }
13181496}
0 commit comments