Skip to content

Commit b59a18a

Browse files
fix: Improved the LTI tool response parser and mitigated XSS vulnerabilities
1 parent 525c1e6 commit b59a18a

4 files changed

Lines changed: 51 additions & 39 deletions

File tree

HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2-
using System.Collections.Generic;
2+
using System.Linq;
33
using System.IdentityModel.Tokens.Jwt;
4+
using System.Text.Encodings.Web;
45
using System.Text.Json;
6+
using System.Text.Unicode;
57
using System.Threading.Tasks;
68
using HwProj.APIGateway.API.Lti.Configuration;
79
using HwProj.APIGateway.API.Lti.Services;
@@ -25,12 +27,12 @@ ILtiKeyService ltiKeyService
2527
[AllowAnonymous]
2628
public async Task<IActionResult> OnDeepLinkingReturnAsync([FromForm] IFormCollection form)
2729
{
28-
if (!form.ContainsKey("JWT"))
30+
if (!form.TryGetValue("JWT", out var jwtValue))
2931
{
3032
return BadRequest("Missing JWT parameter");
3133
}
3234

33-
string tokenString = form["JWT"]!;
35+
var tokenString = jwtValue.ToString();
3436
var handler = new JwtSecurityTokenHandler();
3537

3638
if (!handler.CanReadToken(tokenString))
@@ -48,78 +50,87 @@ public async Task<IActionResult> OnDeepLinkingReturnAsync([FromForm] IFormCollec
4850
}
4951

5052
var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint);
53+
JwtSecurityToken validatedToken;
5154

5255
try
5356
{
5457
handler.ValidateToken(tokenString, new TokenValidationParameters
5558
{
5659
ValidateIssuer = true,
57-
ValidIssuer = unverifiedToken.Issuer,
58-
60+
ValidIssuer = tool.issuer,
5961
ValidateAudience = true,
6062
ValidAudience = ltiPlatformOptions.Value.Issuer,
61-
6263
ValidateLifetime = true,
6364
ClockSkew = TimeSpan.FromMinutes(5),
64-
6565
ValidateIssuerSigningKey = true,
6666
IssuerSigningKeys = signingKeys
67-
}, out var validatedToken);
67+
}, out var secToken);
68+
69+
validatedToken = (JwtSecurityToken)secToken;
6870
}
6971
catch (Exception ex)
7072
{
7173
return BadRequest($"Token signature validation failed: {ex.Message}");
7274
}
7375

7476
const string itemsClaimName = "https://purl.imsglobal.org/spec/lti-dl/claim/content_items";
75-
76-
var resultList = new List<object>();
7777

78-
if (unverifiedToken.Payload.TryGetValue(itemsClaimName, out var itemsObject))
79-
{
80-
var jsonString = itemsObject.ToString();
81-
if (!string.IsNullOrEmpty(jsonString))
82-
{
83-
using var doc = JsonDocument.Parse(jsonString);
84-
if (doc.RootElement.ValueKind == JsonValueKind.Array)
85-
{
86-
foreach (var rawItem in doc.RootElement.EnumerateArray())
87-
{
88-
resultList.Add(rawItem.Clone().ToString());
89-
}
90-
}
91-
}
92-
}
78+
var itemsClaims = validatedToken.Claims
79+
.Where(c => c.Type == itemsClaimName)
80+
.Select(c => c.Value)
81+
.ToList();
9382

94-
if (resultList.Count == 0)
83+
if (itemsClaims.Count == 0)
9584
{
9685
return Content("<script>window.close();</script>", "text/html");
9786
}
9887

99-
var responsePayloadJson = JsonSerializer.Serialize(resultList);
88+
string safeJsonPayload;
89+
var options = new JsonSerializerOptions
90+
{
91+
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
92+
};
93+
94+
if (itemsClaims.Count == 1)
95+
{
96+
var singleParsed = JsonSerializer.Deserialize<JsonElement>(itemsClaims[0]);
97+
98+
safeJsonPayload = singleParsed.ValueKind ==JsonValueKind.Array ?
99+
JsonSerializer.Serialize(singleParsed, options) : JsonSerializer.Serialize(new[] { singleParsed }, options);
100+
}
101+
else
102+
{
103+
var elements = itemsClaims
104+
.Select(v => JsonSerializer.Deserialize<JsonElement>(v))
105+
.ToList();
106+
safeJsonPayload = JsonSerializer.Serialize(elements, options);
107+
}
100108

101-
// language=html
102109
var htmlResponse = $@"
103110
<!DOCTYPE html>
104111
<html>
105112
<head><title>Processing LTI Return...</title></head>
106-
<body>
107-
<p>Задача выбрана. Возвращаемся в HwProj...</p>
113+
<body
114+
<script type=""application/json"" id=""lti-payload"">
115+
{safeJsonPayload}
116+
</script>
117+
108118
<script>
109-
var payload = {responsePayloadJson};
110-
111-
function sendAndClose() {{
119+
try {{
120+
var payloadElement = document.getElementById('lti-payload');
121+
var payload = JSON.parse(payloadElement.textContent);
122+
112123
if (window.opener) {{
113124
window.opener.postMessage({{
114-
type: 'LTI_DEEP_LINK_SUCCESS', // Уникальный тип события, который слушает ваш React/Angular
125+
type: 'LTI_DEEP_LINK_SUCCESS',
115126
payload: payload
116-
}}, '*'); // В продакшене вместо '*' лучше указать домен HwProj
127+
}}, '*'); // В продакшене заменить '*' на конкретный домен
117128
}}
118-
129+
}} catch (e) {{
130+
console.error('Ошибка обработки данных LTI:', e);
131+
}} finally {{
119132
window.close();
120133
}}
121-
122-
sendAndClose();
123134
</script>
124135
</body>
125136
</html>";

HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace HwProj.APIGateway.API.Lti.DTOs;
22

33
public record LtiToolDto(
44
string Name,
5+
string issuer,
56
string ClientId,
67
string JwksEndpoint,
78
string InitiateLoginUri,

HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public static LtiToolDto LtiToolConfigToDto(this LtiToolConfig t)
99
{
1010
return new LtiToolDto(
1111
t.Name,
12+
t.Issuer,
1213
t.ClientId,
1314
t.JwksEndpoint,
1415
t.InitiateLoginUri,

HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ public async Task<IActionResult> PostAndRateSolutionForLti(long taskId,
112112

113113
await _solutionsService.RateSolutionAsync(solutionId, lecturerId!, rateSolutionModel.Rating, rateSolutionModel.LecturerComment);
114114
return Ok();
115-
116115
}
117116

118117
[HttpPost("rateSolution/{solutionId}")]

0 commit comments

Comments
 (0)